set invoke in nama

This commit is contained in:
2026-05-04 11:21:49 +03:30
parent a486127ade
commit 6a48f203a9
17 changed files with 711 additions and 240 deletions
+8 -1
View File
@@ -16,9 +16,16 @@
"ARVANCLOUD", "ARVANCLOUD",
"autoincrement", "autoincrement",
"Cardex", "Cardex",
"consfee",
"fkey", "fkey",
"iban", "iban",
"indatim",
"inno", "inno",
"MAYKET" "inty",
"irtaxid",
"MAYKET",
"setm",
"spro",
"sstid"
] ]
} }
+2 -1
View File
@@ -167,13 +167,14 @@ enum TspProviderResponseStatus {
FAILURE FAILURE
NOT_SEND NOT_SEND
QUEUED QUEUED
REVOKED
} }
enum TspProviderRequestType { enum TspProviderRequestType {
MAIN MAIN
UPDATE UPDATE
REVOKE REVOKE
REMOVE RETURN
} }
enum TspProviderCustomerType { enum TspProviderCustomerType {
+3 -2
View File
@@ -261,7 +261,8 @@ export const TspProviderResponseStatus = {
SUCCESS: 'SUCCESS', SUCCESS: 'SUCCESS',
FAILURE: 'FAILURE', FAILURE: 'FAILURE',
NOT_SEND: 'NOT_SEND', NOT_SEND: 'NOT_SEND',
QUEUED: 'QUEUED' QUEUED: 'QUEUED',
REVOKED: 'REVOKED'
} as const } as const
export type TspProviderResponseStatus = (typeof TspProviderResponseStatus)[keyof typeof TspProviderResponseStatus] export type TspProviderResponseStatus = (typeof TspProviderResponseStatus)[keyof typeof TspProviderResponseStatus]
@@ -271,7 +272,7 @@ export const TspProviderRequestType = {
MAIN: 'MAIN', MAIN: 'MAIN',
UPDATE: 'UPDATE', UPDATE: 'UPDATE',
REVOKE: 'REVOKE', REVOKE: 'REVOKE',
REMOVE: 'REMOVE' RETURN: 'RETURN'
} as const } as const
export type TspProviderRequestType = (typeof TspProviderRequestType)[keyof typeof TspProviderRequestType] export type TspProviderRequestType = (typeof TspProviderRequestType)[keyof typeof TspProviderRequestType]
File diff suppressed because one or more lines are too long
+3
View File
@@ -61,6 +61,9 @@ async function bootstrap() {
'http://localhost:5000', 'http://localhost:5000',
'http://127.0.0.1:5000', 'http://127.0.0.1:5000',
'http://192.168.128.73:5000', 'http://192.168.128.73:5000',
'http://localhost:5005',
'http://127.0.0.1:5005',
'http://192.168.128.73:5005',
] ]
const envOrigins = process.env.CORS_ORIGINS const envOrigins = process.env.CORS_ORIGINS
? process.env.CORS_ORIGINS.split(',') ? process.env.CORS_ORIGINS.split(',')
@@ -1,22 +1,13 @@
import { SaleInvoiceTspModule } from '@/modules/tspProviders/sales-invoice-tsp.module'
import { PrismaModule } from '@/prisma/prisma.module' import { PrismaModule } from '@/prisma/prisma.module'
import { Module } from '@nestjs/common' import { Module } from '@nestjs/common'
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 { StatisticsController } from './saleInvoices.controller' import { StatisticsController } from './saleInvoices.controller'
import { SaleInvoicesService } from './saleInvoices.service' import { SaleInvoicesService } from './saleInvoices.service'
@Module({ @Module({
imports: [PrismaModule], imports: [PrismaModule, SaleInvoiceTspModule],
controllers: [StatisticsController], controllers: [StatisticsController],
providers: [ providers: [SaleInvoicesService],
SaleInvoicesService,
HttpClientUtil,
SalesInvoiceTspService,
SalesInvoiceTspSwitchService,
NamaProviderSwitchAdapter,
],
exports: [SaleInvoicesService], exports: [SaleInvoicesService],
}) })
export class ConsumerSaleInvoicesModule {} export class ConsumerSaleInvoicesModule {}
@@ -66,13 +66,16 @@ export const getPartnerFirstRemainingLicense = async (
id: true, id: true,
charge_transaction_id: true, charge_transaction_id: true,
accounts_limit: true, accounts_limit: true,
parent: {
select: {
name: true,
},
},
}, },
}) })
if (!license) { if (!license) {
throw new BadRequestException( throw new BadRequestException(`لایسنس فعالی برای ${license.parent.name} وجود ندارد.`)
'لایسنس فعال و استفاده نشده برای این شریک تجاری وجود ندارد.',
)
} }
return license return license
@@ -85,9 +88,7 @@ export const ensurePartnerHasRemainingLicense = async (
const quota = await getPartnerRemainingLicenses(prisma, params) const quota = await getPartnerRemainingLicenses(prisma, params)
if (quota.remaining_count <= 0) { if (quota.remaining_count <= 0) {
throw new BadRequestException( throw new BadRequestException('لایسنس فعالی برای این شریک تجاری وجود ندارد.')
'لایسنس فعال و استفاده نشده برای این شریک تجاری وجود ندارد.',
)
} }
return quota return quota
@@ -39,6 +39,11 @@ export class SalesInvoicesController {
return this.salesInvoicesService.send(id, posInfo) return this.salesInvoicesService.send(id, posInfo)
} }
@Post(':id/revoke')
revoke(@Param('id') id: string, @PosInfo() posInfo: IPosPayload) {
return this.salesInvoicesService.revoke(id, posInfo)
}
// @Post('send/bulk') // @Post('send/bulk')
// sendBulk(@Body() data: SendBulkSalesInvoicesDto, @PosInfo() posInfo: IPosPayload) { // sendBulk(@Body() data: SendBulkSalesInvoicesDto, @PosInfo() posInfo: IPosPayload) {
// return this.salesInvoicesService.sendBulk(data.invoice_ids, posInfo) // return this.salesInvoicesService.sendBulk(data.invoice_ids, posInfo)
@@ -1,19 +1,11 @@
import { SaleInvoiceTspModule } from '@/modules/tspProviders/sales-invoice-tsp.module'
import { Module } from '@nestjs/common' import { Module } from '@nestjs/common'
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 { SalesInvoicesController } from './sales-invoices.controller'
import { SalesInvoicesService } from './sales-invoices.service' import { SalesInvoicesService } from './sales-invoices.service'
@Module({ @Module({
imports: [SaleInvoiceTspModule],
controllers: [SalesInvoicesController], controllers: [SalesInvoicesController],
providers: [ providers: [SalesInvoicesService],
SalesInvoicesService,
HttpClientUtil,
SalesInvoiceTspService,
SalesInvoiceTspSwitchService,
NamaProviderSwitchAdapter,
],
}) })
export class PosSalesInvoicesModule {} export class PosSalesInvoicesModule {}
@@ -273,6 +273,14 @@ export class SalesInvoicesService {
return ResponseMapper.single(invoice) return ResponseMapper.single(invoice)
} }
async revoke(invoiceId: string, posInfo: IPosPayload) {
await this.checkAccessToInvoice(posInfo.consumer_account_id, posInfo.pos_id)
const invoice = await this.salesInvoiceTaxService.revoke(posInfo.pos_id, invoiceId)
return ResponseMapper.single(invoice)
}
async inquiry(consumer_account_id: string, pos_id: string, invoiceId: string) { async inquiry(consumer_account_id: string, pos_id: string, invoiceId: string) {
const consumer_id = await this.checkAccessToInvoice(consumer_account_id, pos_id) const consumer_id = await this.checkAccessToInvoice(consumer_account_id, pos_id)
const invoice = await this.salesInvoiceTaxService.get(invoiceId, pos_id, consumer_id) const invoice = await this.salesInvoiceTaxService.get(invoiceId, pos_id, consumer_id)
@@ -292,9 +292,81 @@ export class TspProviderGetResultDto {
received_at: string received_at: string
} }
export class TspProviderRevokePayloadDto {
@ApiProperty({ required: true, nullable: true })
@IsNumber()
invoice_number: number
@ApiProperty({ required: true, nullable: true })
@IsString()
invoice_id: string
@ApiProperty({ required: true, enum: TspProviderType })
@IsEnum(TspProviderType)
tsp_provider: TspProviderType
@ApiProperty({ required: true, nullable: true })
@IsString()
economic_code: string
@ApiProperty({ required: true, nullable: true })
@IsString()
fiscal_id: string
@ApiProperty({ required: true })
@IsString()
tsp_token: string
@ApiProperty({ required: true })
@IsString()
last_attempt_tax_id: string
}
export class TspProviderRevokeResponseDto {
@ApiProperty({ required: true, nullable: true })
@IsString()
invoice_id: string
@ApiProperty({ required: true })
@IsString()
tax_id: string
@ApiProperty({ enum: TspProviderResponseStatus })
@IsEnum(TspProviderResponseStatus)
status: TspProviderResponseStatus
@ApiProperty({ required: true })
@IsBoolean()
hasError: boolean
@ApiProperty({ required: false, nullable: true })
@IsOptional()
@IsString()
message?: string | null
@ApiProperty({ required: false, nullable: true, type: Object })
@IsOptional()
@IsObject()
request_payload?: unknown
@ApiProperty({ required: false, nullable: true, type: Object })
@IsOptional()
@IsObject()
response_payload?: unknown
@ApiProperty({ required: true })
@IsDateString()
sent_at: string
@ApiProperty({ required: true })
@IsDateString()
received_at: string
}
export interface IProviderSwitchAdapter { export interface IProviderSwitchAdapter {
readonly code: string readonly code: string
send(payload: TspProviderSendPayloadDto): Promise<TspProviderSendItemResultDto> send(payload: TspProviderSendPayloadDto): Promise<TspProviderSendItemResultDto>
sendBulk(payloads: TspProviderSendPayloadDto[]): Promise<TspProviderBulkSendResultDto[]> sendBulk(payloads: TspProviderSendPayloadDto[]): Promise<TspProviderBulkSendResultDto[]>
get(invoiceId: string, tsp_token: string): Promise<TspProviderGetResultDto> get(invoiceId: string, tsp_token: string): Promise<TspProviderGetResultDto>
revoke(payload: TspProviderRevokePayloadDto): Promise<TspProviderRevokeResponseDto>
} }
@@ -2,6 +2,8 @@ import { Injectable } from '@nestjs/common'
import { import {
TspProviderBulkSendResultDto, TspProviderBulkSendResultDto,
TspProviderGetResultDto, TspProviderGetResultDto,
TspProviderRevokePayloadDto,
TspProviderRevokeResponseDto,
TspProviderSendItemResultDto, TspProviderSendItemResultDto,
TspProviderSendPayloadDto, TspProviderSendPayloadDto,
} from './dto/provider-switch.dto' } from './dto/provider-switch.dto'
@@ -56,4 +58,11 @@ export class SalesInvoiceTspSwitchService {
const adapter = this.resolveSwitch(providerCode) const adapter = this.resolveSwitch(providerCode)
return await adapter.get(invoiceId, tspToken) return await adapter.get(invoiceId, tspToken)
} }
async revoke(
payload: TspProviderRevokePayloadDto,
): Promise<TspProviderRevokeResponseDto> {
const adapter = this.resolveSwitch(payload.tsp_provider)
return adapter.revoke(payload)
}
} }
@@ -0,0 +1,26 @@
import { HttpClientUtil } from '@/common/utils'
import { NamaProviderUtils } from '@/modules/tspProviders/switch/nama/nama-provider.util'
import { PrismaModule } from '@/prisma/prisma.module'
import { Module } from '@nestjs/common'
import { SalesInvoiceTspSwitchService } from './sales-invoice-tsp-switch.service'
import { SalesInvoiceTspService } from './sales-invoice-tsp.service'
import { NamaProviderSwitchAdapter } from './switch/nama/nama-provider.adapter'
@Module({
imports: [PrismaModule],
providers: [
HttpClientUtil,
SalesInvoiceTspService,
SalesInvoiceTspSwitchService,
NamaProviderSwitchAdapter,
NamaProviderUtils,
],
exports: [
HttpClientUtil,
SalesInvoiceTspService,
SalesInvoiceTspSwitchService,
NamaProviderSwitchAdapter,
NamaProviderUtils,
],
})
export class SaleInvoiceTspModule {}
@@ -7,6 +7,8 @@ import {
} from 'generated/prisma/client' } from 'generated/prisma/client'
import { CreateSalesInvoiceDto } from '../pos/sales-invoices/dto/create-sales-invoice.dto' import { CreateSalesInvoiceDto } from '../pos/sales-invoices/dto/create-sales-invoice.dto'
import { import {
TspProviderRevokePayloadDto,
TspProviderRevokeResponseDto,
TspProviderSendItemResultDto, TspProviderSendItemResultDto,
TspProviderSendPayloadDto, TspProviderSendPayloadDto,
} from './dto/provider-switch.dto' } from './dto/provider-switch.dto'
@@ -239,25 +241,40 @@ export class SalesInvoiceTspService {
}, },
}) })
if (!lastAttempt || lastAttempt?.status === TspProviderResponseStatus.QUEUED) { if (!lastAttempt) {
throw new NotFoundException('صورت‌حساب ارسالی فاکتور شما به سامانه یافت نشد.')
}
if (lastAttempt.status === TspProviderResponseStatus.QUEUED) {
throw new BadRequestException( throw new BadRequestException(
'فاکتور هنوز به سامانه مالیاتی ارسال نشده است یا در صف ارسال قرار دارد. لطفا بعدا تلاش کنید.', 'فاکتور هنوز به سامانه مالیاتی ارسال نشده است یا در صف ارسال قرار دارد. لطفا بعدا تلاش کنید.',
) )
} }
let isUpdated = false 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 let isBackFromSale = false
for (let item of dataToUpdate.items) { if (updatedCounts.size !== previousCounts.size) {
const lastAttemptInvoiceItem = lastAttempt.invoice.items.find( isBackFromSale = true
prevItem => prevItem.good_id === item.good_id, } else {
) for (const [goodId, count] of updatedCounts) {
if (!lastAttemptInvoiceItem) { if (previousCounts.get(goodId) !== count) {
isBackFromSale = true isBackFromSale = true
break break
} }
} }
if (dataToUpdate.total_amount !== lastAttempt.invoice.total_amount.toNumber()) {
isUpdated = true
} }
const attempt = await this.prisma.saleInvoiceTspAttempts.create({ const attempt = await this.prisma.saleInvoiceTspAttempts.create({
@@ -308,6 +325,62 @@ export class SalesInvoiceTspService {
} }
} }
async revoke(
pos_id: string,
invoice_id: string,
): Promise<TspProviderRevokeResponseDto> {
const attempt = await this.prisma.saleInvoiceTspAttempts.findFirst({
where: {
invoice_id,
},
orderBy: {
attempt_no: 'desc',
},
take: 1,
select: {
tax_id: true,
status: true,
attempt_no: true,
invoice: {
select: {
invoice_number: true,
},
},
},
})
if (!attempt) {
throw new NotFoundException('صورت‌حساب ارسالی فاکتور شما به سامانه یافت نشد.')
}
if (attempt.status === TspProviderResponseStatus.QUEUED) {
throw new BadRequestException(
'فاکتور هنوز به سامانه مالیاتی ارسال نشده است یا در صف ارسال قرار دارد. لطفا بعدا تلاش کنید.',
)
}
const payload = await this.buildRevokePayload(invoice_id, pos_id)
const result = await this.tspSwitchService.revoke(payload)
if (result) {
await this.prisma.saleInvoiceTspAttempts.create({
data: {
attempt_no: attempt.attempt_no + 1,
invoice_id,
status: TspProviderResponseStatus.REVOKED,
type: TspProviderRequestType.REVOKE,
received_at: new Date(),
request_payload: JSON.parse(JSON.stringify(result.request_payload)),
sent_at: result.sent_at ? new Date(result.sent_at) : new Date(),
response_payload: JSON.parse(JSON.stringify(result)),
tax_id: result.tax_id,
},
})
}
return result
}
private async getPosId(consumer_id: string, invoice_id: string): Promise<string> { private async getPosId(consumer_id: string, invoice_id: string): Promise<string> {
const now = new Date() const now = new Date()
const saleInvoice = await this.prisma.salesInvoice.findUnique({ const saleInvoice = await this.prisma.salesInvoice.findUnique({
@@ -374,6 +447,94 @@ export class SalesInvoiceTspService {
return saleInvoice.pos.id return saleInvoice.pos.id
} }
private async buildRevokePayload(
invoiceId: string,
posId: string,
): Promise<TspProviderRevokePayloadDto> {
const invoice = await this.prisma.salesInvoice.findUnique({
where: {
id: invoiceId,
pos_id: posId,
},
select: {
id: true,
code: true,
total_amount: true,
invoice_date: true,
invoice_number: 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_attempt_tax_id: invoice.tsp_attempts[0].id,
}
}
private async buildPayload( private async buildPayload(
invoiceId: string, invoiceId: string,
posId: string, posId: string,
@@ -4,31 +4,25 @@ import {
FetchRequestInterceptor, FetchRequestInterceptor,
} from '@/common/interceptors/fetch-request.interceptor' } from '@/common/interceptors/fetch-request.interceptor'
import { HttpClientUtil } from '@/common/utils/http-client.util' import { HttpClientUtil } from '@/common/utils/http-client.util'
import { import { TspProviderResponseStatus } from '@/generated/prisma/enums'
CustomerType,
InvoiceTemplateType,
PaymentMethodType,
TspProviderCustomerType,
TspProviderRequestType,
TspProviderResponseStatus,
} from '@/generated/prisma/enums'
import { Injectable, Logger } from '@nestjs/common' import { Injectable, Logger } from '@nestjs/common'
import { import {
CustomerInfoDto,
IProviderSwitchAdapter, IProviderSwitchAdapter,
PaymentInfoDto,
TspProviderBulkSendResultDto, TspProviderBulkSendResultDto,
TspProviderGetResultDto, TspProviderGetResultDto,
TspProviderRevokePayloadDto,
TspProviderRevokeResponseDto,
TspProviderSendItemResultDto, TspProviderSendItemResultDto,
TspProviderSendPayloadDto, TspProviderSendPayloadDto,
} from '../../dto/provider-switch.dto' } from '../../dto/provider-switch.dto'
import { import {
NamaProviderGetResponseDto, NamaProviderGetResponseDto,
NamaProviderPaymentInfoDto,
NamaProviderRequestDto,
NamaProviderResponseStatus, NamaProviderResponseStatus,
NamaProviderRevokeRequestDto,
NamaProviderRevokeResponseDto,
NamaProviderSendItemResponseDto, NamaProviderSendItemResponseDto,
} from './nama-provider.dto' } from './nama-provider.dto'
import { NamaProviderUtils } from './nama-provider.util'
@Injectable() @Injectable()
export class NamaProviderSwitchAdapter implements IProviderSwitchAdapter { export class NamaProviderSwitchAdapter implements IProviderSwitchAdapter {
@@ -43,7 +37,10 @@ export class NamaProviderSwitchAdapter implements IProviderSwitchAdapter {
private readonly sendBulkPath = '/api/v1/invoices' private readonly sendBulkPath = '/api/v1/invoices'
private readonly checkBulkStatus = '/api/v1/invoices/check-by-uuids' private readonly checkBulkStatus = '/api/v1/invoices/check-by-uuids'
private readonly getPath = '/api/v1/invoices' private readonly getPath = '/api/v1/invoices'
constructor(private readonly httpClient: HttpClientUtil) {} constructor(
private readonly httpClient: HttpClientUtil,
private readonly namaProviderUtils: NamaProviderUtils,
) {}
private get baseUrl() { private get baseUrl() {
return this.sandboxBaseUrl return this.sandboxBaseUrl
@@ -77,7 +74,7 @@ export class NamaProviderSwitchAdapter implements IProviderSwitchAdapter {
} }
async send(payload: TspProviderSendPayloadDto): Promise<TspProviderSendItemResultDto> { async send(payload: TspProviderSendPayloadDto): Promise<TspProviderSendItemResultDto> {
const mappedRequest = this.mapToNamaRequestDto(payload) const mappedRequest = this.namaProviderUtils.mapToNamaRequestDto(payload)
try { try {
console.log(mappedRequest) console.log(mappedRequest)
@@ -101,7 +98,7 @@ export class NamaProviderSwitchAdapter implements IProviderSwitchAdapter {
response_payload: providerResponse, response_payload: providerResponse,
received_at: providerResponse.tsp_update_time, received_at: providerResponse.tsp_update_time,
tax_id: providerResponse.tax_id, tax_id: providerResponse.tax_id,
status: this.mapResponseStatus( status: this.namaProviderUtils.mapResponseStatus(
providerResponse.status as NamaProviderResponseStatus, providerResponse.status as NamaProviderResponseStatus,
), ),
} }
@@ -156,7 +153,7 @@ export class NamaProviderSwitchAdapter implements IProviderSwitchAdapter {
sent_at: new Date().toISOString(), sent_at: new Date().toISOString(),
received_at: providerResponse.tsp_update_time || new Date().toISOString(), received_at: providerResponse.tsp_update_time || new Date().toISOString(),
tax_id: providerResponse.tax_id, tax_id: providerResponse.tax_id,
status: this.mapResponseStatus( status: this.namaProviderUtils.mapResponseStatus(
providerResponse.status as NamaProviderResponseStatus, providerResponse.status as NamaProviderResponseStatus,
), ),
} }
@@ -177,164 +174,52 @@ export class NamaProviderSwitchAdapter implements IProviderSwitchAdapter {
} }
} }
private mapResponseStatus( async revoke(
status: NamaProviderResponseStatus, payload: TspProviderRevokePayloadDto,
): TspProviderResponseStatus { ): Promise<TspProviderRevokeResponseDto> {
switch (status.toUpperCase()) { const mappedRequest: NamaProviderRevokeRequestDto =
case NamaProviderResponseStatus.SUCCESS: this.namaProviderUtils.mapRevokeToNamaRequestDto(payload)
case NamaProviderResponseStatus.POSTED:
return TspProviderResponseStatus.SUCCESS
case NamaProviderResponseStatus.PENDING:
case NamaProviderResponseStatus.IN_PROGRESS:
return TspProviderResponseStatus.QUEUED
case NamaProviderResponseStatus.FAILED:
return TspProviderResponseStatus.FAILURE
default:
return TspProviderResponseStatus.QUEUED
}
}
private mapToNamaRequestDto( try {
payload: TspProviderSendPayloadDto, const response = await this.httpClient.request(
): NamaProviderRequestDto { this.buildSendUrl(),
return { {
uuid: payload.invoice_id, method: 'POST',
economic_code: payload.economic_code, body: JSON.stringify(mappedRequest),
fiscal_id: payload.fiscal_id,
payment: this.mapPayments(payload.payments),
header: {
ins: this.mapTspProviderRequestType(payload.type),
inp: this.mapInvoiceTemplate(payload.invoice_template),
inty: this.mapTspProviderCustomerType(payload.customer_type),
inno: payload.invoice_number.toString(),
tins: '',
indatim: payload.invoice_date.getTime() + '',
setm: '1',
...this.mapCustomerInfo(payload.customer),
}, },
body: payload.items.map(item => ({ this.createRequestInterceptors(payload.tsp_token),
sstid: item.sku, )
vra: item.sku_vat, const providerResponse: NamaProviderRevokeResponseDto = await response.json()
// sstt: item.unit_type, this.logger.debug('NAMA provider response', providerResponse)
fee: String(item.unit_price),
dis: String(item.discount), const result: TspProviderRevokeResponseDto = {
mu: item.measure_unit, invoice_id: payload.invoice_id,
am: String(item.quantity), request_payload: mappedRequest,
consfee: '0', hasError: !response.ok,
bros: '0', message: providerResponse.message,
spro: '0', sent_at: new Date().toISOString(),
})), response_payload: providerResponse,
} received_at: providerResponse.tsp_update_time,
tax_id: providerResponse.tax_id,
status: this.namaProviderUtils.mapResponseStatus(
providerResponse.status as NamaProviderResponseStatus,
),
} }
private mapTspProviderRequestType(type: TspProviderRequestType) { return result
switch (type) { } catch (err) {
case TspProviderRequestType.MAIN: this.logger.error('NAMA Revoke failed', err)
return '1' const failure: TspProviderRevokeResponseDto = {
case TspProviderRequestType.UPDATE: invoice_id: payload.invoice_id,
return '2' request_payload: mappedRequest,
case TspProviderRequestType.REVOKE: hasError: true,
return '3' message: (err as Error).message,
case TspProviderRequestType.REMOVE: sent_at: new Date().toISOString(),
return '4' received_at: new Date().toISOString(),
tax_id: '',
status: TspProviderResponseStatus.FAILURE,
}
return failure
} }
} }
private mapInvoiceTemplate(template: InvoiceTemplateType) {
switch (template) {
case InvoiceTemplateType.SALE:
return '1'
case InvoiceTemplateType.FX_SALE:
return '2'
case InvoiceTemplateType.GOLD_JEWELRY:
return '3'
case InvoiceTemplateType.CONTRACT:
return '4'
case InvoiceTemplateType.UTILITY:
return '5'
case InvoiceTemplateType.AIR_TICKET:
return '6'
case InvoiceTemplateType.EXPORT:
return '7'
case InvoiceTemplateType.BILL_OF_LADING:
return '8'
case InvoiceTemplateType.PETROCHEMICAL:
return '9'
case InvoiceTemplateType.COMMODITY_EXCHANGE:
return '11'
case InvoiceTemplateType.INSURANCE:
return '13'
}
}
private mapTspProviderCustomerType(type?: TspProviderCustomerType) {
switch (type) {
case TspProviderCustomerType.Unknown:
return '1'
case TspProviderCustomerType.Known:
return '2'
default:
return '3'
}
}
private mapCustomerType(type?: CustomerType) {
switch (type) {
case CustomerType.INDIVIDUAL:
return '1'
case CustomerType.LEGAL:
return '2'
default:
return '5'
}
}
private mapCustomerInfo(customer?: CustomerInfoDto) {
const info = {} as any
info.tob = this.mapCustomerType(customer?.type)
if (customer?.type === CustomerType.LEGAL && customer.legal_info) {
info.name = customer.legal_info.name
info.bid = customer.legal_info.economic_code
info.address = ''
} else if (customer?.type === CustomerType.INDIVIDUAL && customer.individual_info) {
info.name = `${customer.individual_info?.first_name || ''} ${
customer.individual_info?.last_name || ''
}`.trim()
info.bid = customer.individual_info.national_id
info.mobile = customer.individual_info.mobile_number
info.address = ''
}
return info
}
private mapPaymentMethod(method: PaymentMethodType): number {
switch (method) {
case PaymentMethodType.CHEQUE:
return 1
case PaymentMethodType.SET_OFF:
return 2
case PaymentMethodType.CASH:
return 3
case PaymentMethodType.TERMINAL:
return 4
case PaymentMethodType.PAYMENT_GATEWAY:
return 5
case PaymentMethodType.CARD:
return 6
case PaymentMethodType.BANK:
return 7
case PaymentMethodType.OTHER:
return 8
}
}
private mapPayments(payments: PaymentInfoDto[]): NamaProviderPaymentInfoDto[] {
return payments.map(payment => ({
pmt: this.mapPaymentMethod(payment.payment_method),
pv: payment.amount,
trn: payment.tracking_code,
pcn: payment.card_number,
pdt: payment.paid_at ? String(payment.paid_at.getTime()) : undefined,
}))
}
} }
@@ -5,6 +5,7 @@ import {
IsArray, IsArray,
IsEnum, IsEnum,
IsNumber, IsNumber,
IsObject,
IsOptional, IsOptional,
IsString, IsString,
Min, Min,
@@ -46,11 +47,14 @@ export class NamaProviderPaymentInfoDto {
} }
export class NamaProviderBodyItemDto { export class NamaProviderBodyItemDto {
@ApiProperty() @ApiProperty({ required: true, description: 'شناسه کالا / خدمت' })
@IsString() @IsString()
sstid: string sstid: string
@ApiProperty() @ApiProperty({
required: true,
description: `نرخ مالیات بر ارزش افزوده`,
})
@IsString() @IsString()
vra: string vra: string
@@ -58,45 +62,63 @@ export class NamaProviderBodyItemDto {
// @IsString() // @IsString()
// sstt: string // sstt: string
@ApiProperty() @ApiProperty({
required: true,
description: 'مبلغ واحد',
})
@IsString() @IsString()
fee: string fee: string
@ApiProperty() @ApiProperty({
required: true,
description: 'مبلغ تخفیف',
})
@IsString() @IsString()
dis: string dis: string
@ApiProperty() @ApiProperty({
required: true,
description: 'واحد اندازه گیری',
})
@IsString() @IsString()
mu: string mu: string
@ApiProperty() @ApiProperty({
required: true,
description: 'تعداد / مقدار',
})
@IsString() @IsString()
am: string am: string
@ApiProperty() @ApiProperty({
description: ' اجرت ساخت (طلا)',
})
@IsString() @IsString()
consfee: string consfee: string
@ApiProperty() @ApiProperty({
description: 'حق العمل',
})
@IsString() @IsString()
bros: string bros: string
@ApiProperty() @ApiProperty({
description: 'سود فروشنده',
})
@IsString() @IsString()
spro: string spro: string
} }
export class NamaProviderHeaderDto { export class NamaProviderHeaderDto {
@ApiProperty({ required: true }) @ApiProperty({ required: true, description: 'موضوع صورتحساب' })
@IsString() @IsString()
ins: string ins: string
@ApiProperty() @ApiProperty({ required: true, description: 'الگوی صورتحساب' })
@IsString() @IsString()
inp: string inp: string
@ApiProperty({ required: true }) @ApiProperty({ required: true, description: 'نوع صورتحساب' })
@IsString() @IsString()
inty: string inty: string
@@ -104,41 +126,46 @@ export class NamaProviderHeaderDto {
// @IsString() // @IsString()
// unique_tax_code: string // unique_tax_code: string
@ApiProperty({ required: true }) @ApiProperty({ required: true, description: 'سریال صورتحساب داخلی حافظه مالیاتی' })
@IsString() @IsString()
inno: string inno: string
@ApiProperty({ required: true }) // @ApiProperty({ required: true })
@IsString() // @IsString()
tins: string // tins: string
@ApiProperty({ required: true }) @ApiProperty({
required: true,
description: 'تاریخ و زمان صدور صورتحساب به صورت timestamp',
})
@IsString() @IsString()
indatim: string indatim: string
@ApiProperty({ required: true }) @ApiProperty({ required: true, description: 'نام مشتری' })
@IsString() @IsString()
name: string name: string
@ApiProperty({ required: true }) @ApiProperty({ required: true, description: 'نوع شخصیت خریدار' })
@IsString() @IsString()
tob: string tob: string
@ApiProperty() @ApiProperty({ description: 'آدرس' })
@IsOptional() @IsOptional()
@IsString() @IsString()
address?: string address?: string
@ApiProperty() @ApiProperty({ description: 'شماره موبایل' })
@IsOptional() @IsOptional()
@IsString() @IsString()
mobile?: string mobile?: string
@ApiProperty() @ApiProperty({
description: `شماره ملی / شناسه ملی / شناسه مشارکت مدنی / کد فراگیر خریدار`,
})
@IsString() @IsString()
bid: string bid: string
@ApiProperty({ required: true }) @ApiProperty({ required: true, description: 'روش تسویه' })
@IsString() @IsString()
setm: string setm: string
} }
@@ -259,3 +286,90 @@ export class NamaProviderGetResponseDto {
@IsString() @IsString()
tsp_update_time: string tsp_update_time: string
} }
export class NamaProviderRevokeHeaderDto {
@ApiProperty({ required: true, description: 'موضوع صورتحساب' })
@IsString()
ins: string
@ApiProperty({ required: true, description: 'سریال صورتحساب داخلی حافظه مالیاتی' })
@IsString()
inno: string
@ApiProperty({
required: true,
description: 'آخرین شماره مالیاتی دریافت شده مربوط به فاکتور',
})
@IsString()
irtaxid: string
}
export class NamaProviderRevokeRequestDto {
// @ApiProperty({ type: [NamaProviderBodyItemDto] })
// @IsArray()
// @ValidateNested({ each: true })
// @Type(() => NamaProviderBodyItemDto)
// body: NamaProviderBodyItemDto[]
// @ApiProperty({ type: [NamaProviderPaymentInfoDto] })
// @IsArray()
// @ValidateNested({ each: true })
// @Type(() => NamaProviderPaymentInfoDto)
// payment: NamaProviderPaymentInfoDto[]
@ApiProperty({ type: Object, required: true })
@IsObject()
body: {}
@ApiProperty({ type: NamaProviderRevokeHeaderDto })
@ValidateNested()
@Type(() => NamaProviderRevokeHeaderDto)
header: NamaProviderRevokeHeaderDto
@ApiProperty()
@IsString()
uuid: string
@ApiProperty()
@IsString()
economic_code: string
@ApiProperty()
@IsString()
fiscal_id: string
}
export class NamaProviderRevokeResponseDto {
@ApiProperty()
@IsString()
uuid: string
@ApiProperty()
@IsString()
message: string
@ApiProperty()
@IsString()
status: NamaProviderResponseStatus
@ApiProperty()
@IsString()
tax_id: string
@ApiProperty()
@IsString()
inno: string
@ApiProperty({ required: true })
@IsString()
reference_number: string
@ApiProperty({ type: () => NamaProviderApiErrorResponseDto, required: false })
@IsOptional()
@ValidateNested()
@Type(() => NamaProviderApiErrorResponseDto)
tax_api_response?: NamaProviderApiErrorResponseDto
@ApiProperty({ required: false })
@IsOptional()
@IsString()
tsp_update_time: string
}
@@ -0,0 +1,195 @@
import {
CustomerType,
InvoiceTemplateType,
PaymentMethodType,
TspProviderCustomerType,
TspProviderRequestType,
TspProviderResponseStatus,
} from '@/generated/prisma/enums'
import {
CustomerInfoDto,
PaymentInfoDto,
TspProviderRevokePayloadDto,
TspProviderSendPayloadDto,
} from '../../dto/provider-switch.dto'
import {
NamaProviderPaymentInfoDto,
NamaProviderRequestDto,
NamaProviderResponseStatus,
NamaProviderRevokeRequestDto,
} from './nama-provider.dto'
export class NamaProviderUtils {
mapResponseStatus(status: NamaProviderResponseStatus): TspProviderResponseStatus {
switch (status.toUpperCase()) {
case NamaProviderResponseStatus.SUCCESS:
case NamaProviderResponseStatus.POSTED:
return TspProviderResponseStatus.SUCCESS
case NamaProviderResponseStatus.PENDING:
case NamaProviderResponseStatus.IN_PROGRESS:
return TspProviderResponseStatus.QUEUED
case NamaProviderResponseStatus.FAILED:
return TspProviderResponseStatus.FAILURE
default:
return TspProviderResponseStatus.QUEUED
}
}
mapToNamaRequestDto(payload: TspProviderSendPayloadDto): NamaProviderRequestDto {
return {
uuid: payload.invoice_id,
economic_code: payload.economic_code,
fiscal_id: payload.fiscal_id,
payment: this.mapPayments(payload.payments),
header: {
ins: this.mapTspProviderRequestType(payload.type),
inp: this.mapInvoiceTemplate(payload.invoice_template),
inty: this.mapTspProviderCustomerType(payload.customer_type),
inno: payload.invoice_number.toString(),
tins: '',
indatim: payload.invoice_date.getTime() + '',
setm: '1',
...this.mapCustomerInfo(payload.customer),
},
body: payload.items.map(item => ({
sstid: item.sku,
vra: item.sku_vat,
// sstt: item.unit_type,
fee: String(item.unit_price),
dis: String(item.discount),
mu: item.measure_unit,
am: String(item.quantity),
consfee: '0',
bros: '0',
spro: '0',
})),
}
}
mapRevokeToNamaRequestDto(
payload: TspProviderRevokePayloadDto,
): NamaProviderRevokeRequestDto {
return {
uuid: payload.invoice_id,
economic_code: payload.economic_code,
fiscal_id: payload.fiscal_id,
header: {
inno: payload.invoice_number.toString(),
ins: this.mapTspProviderRequestType(TspProviderRequestType.REVOKE),
irtaxid: payload.last_attempt_tax_id,
},
body: {},
}
}
private mapTspProviderRequestType(type: TspProviderRequestType) {
switch (type) {
case TspProviderRequestType.MAIN:
return '1'
case TspProviderRequestType.UPDATE:
return '2'
case TspProviderRequestType.REVOKE:
return '3'
case TspProviderRequestType.RETURN:
return '4'
}
}
private mapInvoiceTemplate(template: InvoiceTemplateType) {
switch (template) {
case InvoiceTemplateType.SALE:
return '1'
case InvoiceTemplateType.FX_SALE:
return '2'
case InvoiceTemplateType.GOLD_JEWELRY:
return '3'
case InvoiceTemplateType.CONTRACT:
return '4'
case InvoiceTemplateType.UTILITY:
return '5'
case InvoiceTemplateType.AIR_TICKET:
return '6'
case InvoiceTemplateType.EXPORT:
return '7'
case InvoiceTemplateType.BILL_OF_LADING:
return '8'
case InvoiceTemplateType.PETROCHEMICAL:
return '9'
case InvoiceTemplateType.COMMODITY_EXCHANGE:
return '11'
case InvoiceTemplateType.INSURANCE:
return '13'
}
}
private mapTspProviderCustomerType(type?: TspProviderCustomerType) {
switch (type) {
case TspProviderCustomerType.Known:
return '1'
case TspProviderCustomerType.Unknown:
return '2'
default:
return '3'
}
}
private mapCustomerType(type?: CustomerType) {
switch (type) {
case CustomerType.INDIVIDUAL:
return '1'
case CustomerType.LEGAL:
return '2'
default:
return '5'
}
}
private mapCustomerInfo(customer?: CustomerInfoDto) {
const info = {} as any
info.tob = this.mapCustomerType(customer?.type)
if (customer?.type === CustomerType.LEGAL && customer.legal_info) {
info.name = customer.legal_info.name
info.bid = customer.legal_info.economic_code
info.address = ''
} else if (customer?.type === CustomerType.INDIVIDUAL && customer.individual_info) {
info.name = `${customer.individual_info?.first_name || ''} ${
customer.individual_info?.last_name || ''
}`.trim()
info.bid = customer.individual_info.national_id
info.mobile = customer.individual_info.mobile_number
info.address = ''
}
return info
}
private mapPaymentMethod(method: PaymentMethodType): number {
switch (method) {
case PaymentMethodType.CHEQUE:
return 1
case PaymentMethodType.SET_OFF:
return 2
case PaymentMethodType.CASH:
return 3
case PaymentMethodType.TERMINAL:
return 4
case PaymentMethodType.PAYMENT_GATEWAY:
return 5
case PaymentMethodType.CARD:
return 6
case PaymentMethodType.BANK:
return 7
case PaymentMethodType.OTHER:
return 8
}
}
private mapPayments(payments: PaymentInfoDto[]): NamaProviderPaymentInfoDto[] {
return payments.map(payment => ({
pmt: this.mapPaymentMethod(payment.payment_method),
pv: payment.amount,
trn: payment.tracking_code,
pcn: payment.card_number,
pdt: payment.paid_at ? String(payment.paid_at.getTime()) : undefined,
}))
}
}