feat: enhance CustomerIndividual and CustomerLegal models with new unique input types

feat: add cache invalidation method for POS middleware

feat: implement Redis caching in POS middleware for improved performance

feat: refactor sales invoice DTOs for POS to include correction functionality

feat: extend SalesInvoicesController to handle invoice corrections and returns

feat: update SalesInvoicesService to support invoice correction and return operations

feat: enhance TSP provider correction functionality with new DTOs and utility methods

fix: improve error handling and logging in Nama provider switch adapter

refactor: streamline invoice payload building in TSP provider utilities
This commit is contained in:
2026-06-06 19:53:00 +03:30
parent 25e589551b
commit f61100bf25
21 changed files with 302 additions and 113 deletions
+5 -4
View File
@@ -1,5 +1,6 @@
import { import {
InvoiceSettlementType, InvoiceSettlementType,
TspProviderRequestType,
TspProviderResponseStatus, TspProviderResponseStatus,
} from '@/generated/prisma/enums' } from '@/generated/prisma/enums'
@@ -153,10 +154,10 @@ export default {
[InvoiceSettlementType.MIXED]: 'نقدی / نسیه', [InvoiceSettlementType.MIXED]: 'نقدی / نسیه',
}, },
TspProviderRequestType: { TspProviderRequestType: {
ORIGINAL: 'اصلی', [TspProviderRequestType.ORIGINAL]: 'اصلی',
CORRECTION: 'اصلاح', [TspProviderRequestType.CORRECTION]: 'اصلاح',
REVOKE: 'ابطال', [TspProviderRequestType.REVOKE]: 'ابطال',
REMOVE: 'حذف', [TspProviderRequestType.RETURN]: 'برگشت از فروش',
}, },
TspProviderCustomerType: { TspProviderCustomerType: {
Unknown: 'ناشناس', Unknown: 'ناشناس',
@@ -1,6 +1,7 @@
import { SharedSaleInvoiceAccessService } from '@/common/services/saleInvoices/sale-invoice-access.service' import { SharedSaleInvoiceAccessService } from '@/common/services/saleInvoices/sale-invoice-access.service'
import { Injectable } from '@nestjs/common' import { PosCorrectionSalesInvoiceDto } from '@/modules/pos/sales-invoices/dto/create-sales-invoice.dto'
import { SalesInvoiceTspService } from '@/modules/tspProviders/sales-invoice-tsp.service' import { SalesInvoiceTspService } from '@/modules/tspProviders/sales-invoice-tsp.service'
import { Injectable } from '@nestjs/common'
@Injectable() @Injectable()
export class SharedSaleInvoiceActionsService { export class SharedSaleInvoiceActionsService {
@@ -45,6 +46,28 @@ export class SharedSaleInvoiceActionsService {
) )
} }
async correction(
data: PosCorrectionSalesInvoiceDto,
consumerAccountId: string,
posId: string,
complexId: string,
businessId: string,
invoiceId: string,
) {
await this.saleInvoiceAccessService.getConsumerIdWithPosAccess(
consumerAccountId,
posId,
)
return this.salesInvoiceTspService.correctionSend(
consumerAccountId,
posId,
complexId,
businessId,
invoiceId,
data,
)
}
async inquiry(consumerAccountId: string, posId: string, invoiceId: string) { async inquiry(consumerAccountId: string, posId: string, invoiceId: string) {
const consumerId = await this.saleInvoiceAccessService.getConsumerIdWithPosAccess( const consumerId = await this.saleInvoiceAccessService.getConsumerIdWithPosAccess(
consumerAccountId, consumerAccountId,
@@ -1,5 +1,5 @@
import type { SaleInvoiceType } from '@/common/interfaces/sale-invoice-payload' import type { SaleInvoiceType } from '@/common/interfaces/sale-invoice-payload'
import { ApiProperty } from '@nestjs/swagger' import { ApiProperty, OmitType } from '@nestjs/swagger'
import { Type } from 'class-transformer' import { Type } from 'class-transformer'
import { import {
ArrayMinSize, ArrayMinSize,
@@ -206,8 +206,6 @@ export class SharedCreateSalesInvoiceDto {
@IsBoolean() @IsBoolean()
send_to_tsp?: boolean send_to_tsp?: boolean
@ApiProperty({ required: true, default: CustomerType.UNKNOWN }) @ApiProperty({ required: true, default: CustomerType.UNKNOWN })
@IsEnum(CustomerType) @IsEnum(CustomerType)
customer_type: CustomerType customer_type: CustomerType
@@ -229,3 +227,8 @@ export class SharedCreateSalesInvoiceDto {
} }
} }
} }
export class SharedCorrectionSalesInvoiceDto extends OmitType(
SharedCreateSalesInvoiceDto,
['customer', 'customer_id', 'send_to_tsp', 'customer_type', 'settlement_type'],
) {}
+2 -2
View File
@@ -18,13 +18,13 @@ export function checkAndDecodeJwtToken(
token = authHeader.slice(7) token = authHeader.slice(7)
} }
} }
if (!token) throw new UnauthorizedException('Missing accessToken cookie') if (!token) throw new UnauthorizedException('توکن احراز هویت ارسال نشده است')
try { try {
const payload = jwtService.decode(token) as AccessTokenPayload const payload = jwtService.decode(token) as AccessTokenPayload
return payload as ITokenPayload return payload as ITokenPayload
} catch { } catch {
return null throw new UnauthorizedException('توکن احراز هویت نامعتبر است')
} }
} }
+1
View File
@@ -20,6 +20,7 @@ export class RedisKeyMaker extends PartnerKeyMaker {
static guildStockKeepingUnitsList = GuildKeyMaker.guildStockKeepingUnitsList static guildStockKeepingUnitsList = GuildKeyMaker.guildStockKeepingUnitsList
static guildGoodsList = GuildKeyMaker.guildGoodsList static guildGoodsList = GuildKeyMaker.guildGoodsList
static posMiddleware = PosKeyMaker.middleware
static posInfo = PosKeyMaker.info static posInfo = PosKeyMaker.info
static posMe = PosKeyMaker.me static posMe = PosKeyMaker.me
static posGoodsList = PosKeyMaker.goodList static posGoodsList = PosKeyMaker.goodList
+4
View File
@@ -1,4 +1,8 @@
export class PosKeyMaker { export class PosKeyMaker {
static middleware(token: string): string {
return `pos:${token}:middleware`
}
static info(posId: string): string { static info(posId: string): string {
return `pos:${posId}:info` return `pos:${posId}:info`
} }
File diff suppressed because one or more lines are too long
@@ -227,6 +227,7 @@ export type CustomerIndividualOrderByWithRelationInput = {
export type CustomerIndividualWhereUniqueInput = Prisma.AtLeast<{ export type CustomerIndividualWhereUniqueInput = Prisma.AtLeast<{
customer_id?: string customer_id?: string
business_activity_id_national_id?: Prisma.CustomerIndividualBusiness_activity_idNational_idCompoundUniqueInput business_activity_id_national_id?: Prisma.CustomerIndividualBusiness_activity_idNational_idCompoundUniqueInput
business_activity_id_postal_code_national_id?: Prisma.CustomerIndividualBusiness_activity_idPostal_codeNational_idCompoundUniqueInput
AND?: Prisma.CustomerIndividualWhereInput | Prisma.CustomerIndividualWhereInput[] AND?: Prisma.CustomerIndividualWhereInput | Prisma.CustomerIndividualWhereInput[]
OR?: Prisma.CustomerIndividualWhereInput[] OR?: Prisma.CustomerIndividualWhereInput[]
NOT?: Prisma.CustomerIndividualWhereInput | Prisma.CustomerIndividualWhereInput[] NOT?: Prisma.CustomerIndividualWhereInput | Prisma.CustomerIndividualWhereInput[]
@@ -239,7 +240,7 @@ export type CustomerIndividualWhereUniqueInput = Prisma.AtLeast<{
business_activity_id?: Prisma.StringFilter<"CustomerIndividual"> | string business_activity_id?: Prisma.StringFilter<"CustomerIndividual"> | string
business_activity?: Prisma.XOR<Prisma.BusinessActivityScalarRelationFilter, Prisma.BusinessActivityWhereInput> business_activity?: Prisma.XOR<Prisma.BusinessActivityScalarRelationFilter, Prisma.BusinessActivityWhereInput>
customer?: Prisma.XOR<Prisma.CustomerScalarRelationFilter, Prisma.CustomerWhereInput> customer?: Prisma.XOR<Prisma.CustomerScalarRelationFilter, Prisma.CustomerWhereInput>
}, "customer_id" | "business_activity_id_national_id"> }, "customer_id" | "business_activity_id_national_id" | "business_activity_id_postal_code_national_id">
export type CustomerIndividualOrderByWithAggregationInput = { export type CustomerIndividualOrderByWithAggregationInput = {
first_name?: Prisma.SortOrderInput | Prisma.SortOrder first_name?: Prisma.SortOrderInput | Prisma.SortOrder
@@ -370,6 +371,12 @@ export type CustomerIndividualBusiness_activity_idNational_idCompoundUniqueInput
national_id: string national_id: string
} }
export type CustomerIndividualBusiness_activity_idPostal_codeNational_idCompoundUniqueInput = {
business_activity_id: string
postal_code: string
national_id: string
}
export type CustomerIndividualCountOrderByAggregateInput = { export type CustomerIndividualCountOrderByAggregateInput = {
first_name?: Prisma.SortOrder first_name?: Prisma.SortOrder
last_name?: Prisma.SortOrder last_name?: Prisma.SortOrder
+8 -1
View File
@@ -209,6 +209,7 @@ export type CustomerLegalOrderByWithRelationInput = {
export type CustomerLegalWhereUniqueInput = Prisma.AtLeast<{ export type CustomerLegalWhereUniqueInput = Prisma.AtLeast<{
customer_id?: string customer_id?: string
business_activity_id_economic_code?: Prisma.CustomerLegalBusiness_activity_idEconomic_codeCompoundUniqueInput business_activity_id_economic_code?: Prisma.CustomerLegalBusiness_activity_idEconomic_codeCompoundUniqueInput
business_activity_id_postal_code_registration_number?: Prisma.CustomerLegalBusiness_activity_idPostal_codeRegistration_numberCompoundUniqueInput
AND?: Prisma.CustomerLegalWhereInput | Prisma.CustomerLegalWhereInput[] AND?: Prisma.CustomerLegalWhereInput | Prisma.CustomerLegalWhereInput[]
OR?: Prisma.CustomerLegalWhereInput[] OR?: Prisma.CustomerLegalWhereInput[]
NOT?: Prisma.CustomerLegalWhereInput | Prisma.CustomerLegalWhereInput[] NOT?: Prisma.CustomerLegalWhereInput | Prisma.CustomerLegalWhereInput[]
@@ -219,7 +220,7 @@ export type CustomerLegalWhereUniqueInput = Prisma.AtLeast<{
business_activity_id?: Prisma.StringFilter<"CustomerLegal"> | string business_activity_id?: Prisma.StringFilter<"CustomerLegal"> | string
business_activity?: Prisma.XOR<Prisma.BusinessActivityScalarRelationFilter, Prisma.BusinessActivityWhereInput> business_activity?: Prisma.XOR<Prisma.BusinessActivityScalarRelationFilter, Prisma.BusinessActivityWhereInput>
customer?: Prisma.XOR<Prisma.CustomerScalarRelationFilter, Prisma.CustomerWhereInput> customer?: Prisma.XOR<Prisma.CustomerScalarRelationFilter, Prisma.CustomerWhereInput>
}, "customer_id" | "business_activity_id_economic_code"> }, "customer_id" | "business_activity_id_economic_code" | "business_activity_id_postal_code_registration_number">
export type CustomerLegalOrderByWithAggregationInput = { export type CustomerLegalOrderByWithAggregationInput = {
name?: Prisma.SortOrderInput | Prisma.SortOrder name?: Prisma.SortOrderInput | Prisma.SortOrder
@@ -332,6 +333,12 @@ export type CustomerLegalBusiness_activity_idEconomic_codeCompoundUniqueInput =
economic_code: string economic_code: string
} }
export type CustomerLegalBusiness_activity_idPostal_codeRegistration_numberCompoundUniqueInput = {
business_activity_id: string
postal_code: string
registration_number: string
}
export type CustomerLegalCountOrderByAggregateInput = { export type CustomerLegalCountOrderByAggregateInput = {
name?: Prisma.SortOrder name?: Prisma.SortOrder
economic_code?: Prisma.SortOrder economic_code?: Prisma.SortOrder
@@ -6,6 +6,10 @@ import { Injectable } from '@nestjs/common'
export class PosCacheInvalidationService { export class PosCacheInvalidationService {
constructor(private readonly redisService: RedisService) {} constructor(private readonly redisService: RedisService) {}
async invalidatePosMiddleware(token: string): Promise<void> {
this.redisService.delete(PosKeyMaker.middleware(token))
}
async invalidatePosGoodsList( async invalidatePosGoodsList(
guildId: string, guildId: string,
businessActivityId: string, businessActivityId: string,
+40 -2
View File
@@ -1,7 +1,9 @@
import { RedisKeyMaker } from '@/common/utils'
import { checkAndDecodeJwtToken } from '@/common/utils/jwt-user.util' import { checkAndDecodeJwtToken } from '@/common/utils/jwt-user.util'
import { ConsumerRole, POSStatus } from '@/generated/prisma/enums' import { ConsumerRole, POSStatus } from '@/generated/prisma/enums'
import { PosSelect, PosWhereInput } from '@/generated/prisma/models' import { PosSelect, PosWhereInput } from '@/generated/prisma/models'
import { PrismaService } from '@/prisma/prisma.service' import { PrismaService } from '@/prisma/prisma.service'
import { RedisService } from '@/redis/redis.service'
import { import {
ForbiddenException, ForbiddenException,
Injectable, Injectable,
@@ -16,15 +18,38 @@ export class PosMiddleware implements NestMiddleware {
constructor( constructor(
private prisma: PrismaService, private prisma: PrismaService,
private jwtService: JwtService, private jwtService: JwtService,
private redisService: RedisService,
) {} ) {}
async use(req: Request, _res: Response, next: NextFunction) { async use(req: Request, _res: Response, next: NextFunction) {
const doForbidden = () => { const doForbidden = () => {
throw new ForbiddenException('شما دسترسی لازم را ندارید') throw new ForbiddenException('شما دسترسی لازم را ندارید')
} }
const token = req.cookies?.accessToken
const cacheKey = RedisKeyMaker.posMiddleware(token)
let cashedPosData: unknown
try {
cashedPosData = await this.redisService.getJson(cacheKey)
} catch (_) {}
const tokenAccount = checkAndDecodeJwtToken(req, this.jwtService)
req.decodedToken = tokenAccount!
if (
cashedPosData &&
typeof cashedPosData === 'object' &&
'pos_id' in cashedPosData &&
'complex_id' in cashedPosData &&
'business_id' in cashedPosData &&
'guild_id' in cashedPosData &&
'consumer_account_id' in cashedPosData
) {
req.posData = cashedPosData as any
return next()
}
return this.prisma.$transaction(async tx => { return this.prisma.$transaction(async tx => {
try { try {
const tokenAccount = checkAndDecodeJwtToken(req, this.jwtService)
const { type, role, account_id } = tokenAccount! const { type, role, account_id } = tokenAccount!
const posSelect: PosSelect = { const posSelect: PosSelect = {
id: true, id: true,
@@ -103,6 +128,20 @@ export class PosMiddleware implements NestMiddleware {
return doForbidden() return doForbidden()
} }
try {
await this.redisService.setJson(
cacheKey,
{
pos_id: posId,
complex_id: pos.complex.id,
business_id: pos.complex.business_activity.id,
guild_id: pos.complex.business_activity.guild_id,
consumer_account_id: account_id,
},
60 * 60,
)
} catch (_) {}
req.posData = { req.posData = {
pos_id: posId, pos_id: posId,
complex_id: pos.complex.id, complex_id: pos.complex.id,
@@ -110,7 +149,6 @@ export class PosMiddleware implements NestMiddleware {
guild_id: pos.complex.business_activity.guild_id, guild_id: pos.complex.business_activity.guild_id,
consumer_account_id: account_id, consumer_account_id: account_id,
} }
req.decodedToken = tokenAccount!
return next() return next()
} catch (error) { } catch (error) {
@@ -1,6 +1,7 @@
import { SharedCreateSalesInvoiceDto } from '@/common/services/saleInvoices/sale-invoice-create.dto' import {
import { PartialType } from '@nestjs/swagger' SharedCorrectionSalesInvoiceDto,
SharedCreateSalesInvoiceDto,
} from '@/common/services/saleInvoices/sale-invoice-create.dto'
export class CreateSalesInvoiceDto extends SharedCreateSalesInvoiceDto {} export class PosCreateSalesInvoiceDto extends SharedCreateSalesInvoiceDto {}
export class PosCorrectionSalesInvoiceDto extends SharedCorrectionSalesInvoiceDto {}
export class UpdateSalesInvoiceDto extends PartialType(CreateSalesInvoiceDto) {}
@@ -2,7 +2,10 @@ import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common'
import { PosInfo } from '@/common/decorators/posInfo.decorator' import { PosInfo } from '@/common/decorators/posInfo.decorator'
import type { IPosPayload } from '@/common/models/posPayload.model' import type { IPosPayload } from '@/common/models/posPayload.model'
import { CreateSalesInvoiceDto } from './dto/create-sales-invoice.dto' import {
PosCorrectionSalesInvoiceDto,
PosCreateSalesInvoiceDto,
} from './dto/create-sales-invoice.dto'
import { SalesInvoicesFilterDto } from './dto/sales-invoices-filter.dto' import { SalesInvoicesFilterDto } from './dto/sales-invoices-filter.dto'
import { SalesInvoicesService } from './sales-invoices.service' import { SalesInvoicesService } from './sales-invoices.service'
@@ -30,7 +33,7 @@ export class SalesInvoicesController {
} }
@Post() @Post()
create(@Body() data: CreateSalesInvoiceDto, @PosInfo() posInfo: IPosPayload) { create(@Body() data: PosCreateSalesInvoiceDto, @PosInfo() posInfo: IPosPayload) {
return this.salesInvoicesService.create(data, posInfo) return this.salesInvoicesService.create(data, posInfo)
} }
@@ -49,6 +52,19 @@ export class SalesInvoicesController {
return this.salesInvoicesService.revoke(id, posInfo) return this.salesInvoicesService.revoke(id, posInfo)
} }
@Post(':id/correction')
correction(
@Param('id') id: string,
@PosInfo() posInfo: IPosPayload,
@Body() data: PosCorrectionSalesInvoiceDto,
) {
return this.salesInvoicesService.correction(id, posInfo, data)
}
@Post(':id/return')
return(@Param('id') id: string, @PosInfo() posInfo: IPosPayload) {
return this.salesInvoicesService.return(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)
@@ -9,7 +9,10 @@ import { ResponseMapper } from 'common/response/response-mapper'
import { Prisma } from 'generated/prisma/client' import { Prisma } from 'generated/prisma/client'
import { TspProviderRequestType, TspProviderResponseStatus } from 'generated/prisma/enums' import { TspProviderRequestType, TspProviderResponseStatus } from 'generated/prisma/enums'
import { SalesInvoiceTspService } from '../../tspProviders/sales-invoice-tsp.service' import { SalesInvoiceTspService } from '../../tspProviders/sales-invoice-tsp.service'
import { CreateSalesInvoiceDto } from './dto/create-sales-invoice.dto' import {
PosCorrectionSalesInvoiceDto,
PosCreateSalesInvoiceDto,
} from './dto/create-sales-invoice.dto'
import { SalesInvoicesFilterDto } from './dto/sales-invoices-filter.dto' import { SalesInvoicesFilterDto } from './dto/sales-invoices-filter.dto'
@Injectable() @Injectable()
@@ -91,7 +94,7 @@ export class SalesInvoicesService {
} else throw new NotFoundException('فاکتور مورد نظر شما یافت نشد.') } else throw new NotFoundException('فاکتور مورد نظر شما یافت نشد.')
} }
async create(data: CreateSalesInvoiceDto, posInfo: IPosPayload) { async create(data: PosCreateSalesInvoiceDto, posInfo: IPosPayload) {
let salesInvoice = await this.sharedSaleInvoiceCreateService.create({ let salesInvoice = await this.sharedSaleInvoiceCreateService.create({
data, data,
businessId: posInfo.business_id, businessId: posInfo.business_id,
@@ -148,6 +151,39 @@ export class SalesInvoicesService {
return ResponseMapper.single(invoice) return ResponseMapper.single(invoice)
} }
async correction(
invoiceId: string,
posInfo: IPosPayload,
data: PosCorrectionSalesInvoiceDto,
) {
const { consumer_account_id, pos_id, business_id, complex_id } = posInfo
const invoice = await this.sharedSaleInvoiceActionsService.correction(
data,
consumer_account_id,
pos_id,
complex_id,
business_id,
invoiceId,
)
return ResponseMapper.single(invoice)
}
async return(invoiceId: string, posInfo: IPosPayload) {
const { consumer_account_id, pos_id, business_id, complex_id } = posInfo
const invoice = await this.sharedSaleInvoiceActionsService.revoke(
consumer_account_id,
pos_id,
complex_id,
business_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 invoice = await this.sharedSaleInvoiceActionsService.inquiry( const invoice = await this.sharedSaleInvoiceActionsService.inquiry(
consumer_account_id, consumer_account_id,
+10 -2
View File
@@ -7,14 +7,22 @@ import { Type } from 'class-transformer'
import { import {
ArrayMinSize, ArrayMinSize,
IsArray, IsArray,
IsDateString,
IsNumber, IsNumber,
IsObject, IsObject,
IsString, IsString,
ValidateNested, ValidateNested,
} from 'class-validator' } from 'class-validator'
import { TspProviderOriginalSendPayloadDto } from './original.dto' import {
TspProviderOriginalResponseDto,
TspProviderOriginalSendPayloadDto,
} from './original.dto'
export class TspProviderCorrectionInvoicePayloadDto { export class TspProviderCorrectionInvoicePayloadDto {
@ApiProperty({ required: true, nullable: true })
@IsDateString()
invoice_date: Date
@ApiProperty({ type: [SharedCreateSalesInvoiceItemDto] }) @ApiProperty({ type: [SharedCreateSalesInvoiceItemDto] })
@IsArray() @IsArray()
@ValidateNested({ each: true }) @ValidateNested({ each: true })
@@ -46,4 +54,4 @@ export class TspProviderCorrectionSendPayloadDto extends TspProviderOriginalSend
last_tax_id: string last_tax_id: string
} }
export class TspProviderCorrectionSendResponseDto {} export class TspProviderCorrectionSendResponseDto extends TspProviderOriginalResponseDto {}
+1 -1
View File
@@ -14,7 +14,7 @@ import {
IsOptional, IsOptional,
IsString, IsString,
Min, Min,
ValidateNested ValidateNested,
} from 'class-validator' } from 'class-validator'
import { import {
CustomerInfoDto, CustomerInfoDto,
@@ -16,6 +16,7 @@ import {
buildOriginalPayload, buildOriginalPayload,
buildRevokePayload, buildRevokePayload,
getOriginalResendAttemptNumber, getOriginalResendAttemptNumber,
getRelatedInvoiceForCorrection,
onResult, onResult,
trySend, trySend,
} from './utils/sales-invoice-tsp.utils' } from './utils/sales-invoice-tsp.utils'
@@ -165,45 +166,13 @@ export class SalesInvoiceTspService {
): Promise<TspProviderActionResponseDto> { ): Promise<TspProviderActionResponseDto> {
const [newInvoice, attempt, correctionPayload] = await this.prisma.$transaction( const [newInvoice, attempt, correctionPayload] = await this.prisma.$transaction(
async tx => { async tx => {
const relatedInvoice = await tx.salesInvoice.findUnique({ const relatedInvoice = await getRelatedInvoiceForCorrection(tx, ref_invoice_id)
where: {
id: ref_invoice_id,
},
include: {
customer: {
select: {
type: true,
},
},
tsp_attempts: {
orderBy: {
attempt_no: 'desc',
},
take: 1,
},
},
})
if (!relatedInvoice) {
throw new NotFoundException('فاکتور مورد نظر یافت نشد.')
}
if (relatedInvoice.type === TspProviderRequestType.REVOKE) {
throw new BadRequestException(
'فاکتور ارسالی قبلا ابطال شده است و امکان ویرایش آن وجود ندارد.',
)
}
if (!relatedInvoice.tax_id) {
throw new BadRequestException(
'فاکتور قبلی همچنان در حال بررسی است و امکان ویرایش آن وجود ندارد.',
)
}
const newInvoice = await this.sharedSaleInvoiceCreateService.create({ const newInvoice = await this.sharedSaleInvoiceCreateService.create({
tx,
data: { data: {
customer_type: relatedInvoice.customer?.type || CustomerType.UNKNOWN, customer_type: relatedInvoice.customer?.type || CustomerType.UNKNOWN,
invoice_date: new Date(), invoice_date: dataToUpdate.invoice_date,
payments: dataToUpdate.payments, payments: dataToUpdate.payments,
items: dataToUpdate.items, items: dataToUpdate.items,
total_amount: dataToUpdate.total_amount, total_amount: dataToUpdate.total_amount,
@@ -40,7 +40,7 @@ export class NamaProviderCorrectionRequestDto {
payment: NamaProviderPaymentInfoDto[] payment: NamaProviderPaymentInfoDto[]
@ApiProperty({ type: NamaProviderCorrectionHeaderDto }) @ApiProperty({ type: NamaProviderCorrectionHeaderDto })
@ValidateNested() @ValidateNested({ each: true })
@Type(() => NamaProviderCorrectionHeaderDto) @Type(() => NamaProviderCorrectionHeaderDto)
header: NamaProviderCorrectionHeaderDto header: NamaProviderCorrectionHeaderDto
@@ -20,6 +20,7 @@ import {
TspProviderBulkSendResultDto, TspProviderBulkSendResultDto,
} from '../../dto/provider-switch.dto' } from '../../dto/provider-switch.dto'
import { import {
NamaProviderCorrectionResponseDto,
NamaProviderGetResponseDto, NamaProviderGetResponseDto,
NamaProviderOriginalResponseDto, NamaProviderOriginalResponseDto,
NamaProviderResponseStatus, NamaProviderResponseStatus,
@@ -38,6 +39,7 @@ export class NamaProviderSwitchAdapter implements IProviderSwitchAdapter {
process.env.NAMA_PROVIDER_PRODUCTION_URL || 'https://api.nama.ir' process.env.NAMA_PROVIDER_PRODUCTION_URL || 'https://api.nama.ir'
private readonly sendPath = '/api/v1/invoices/single-send' private readonly sendPath = '/api/v1/invoices/single-send'
private readonly correctionPath = this.sendPath
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'
@@ -57,6 +59,10 @@ export class NamaProviderSwitchAdapter implements IProviderSwitchAdapter {
return `${this.baseUrl}${this.sendPath}` return `${this.baseUrl}${this.sendPath}`
} }
private buildCorrectionUrl() {
return `${this.baseUrl}${this.correctionPath}`
}
private buildGetUrl(invoiceId: string) { private buildGetUrl(invoiceId: string) {
return `${this.baseUrl}${this.getPath}/${invoiceId}` return `${this.baseUrl}${this.getPath}/${invoiceId}`
} }
@@ -110,9 +116,8 @@ export class NamaProviderSwitchAdapter implements IProviderSwitchAdapter {
return result return result
} catch (err) { } catch (err) {
console.log('err@@@@@@@@@@@@@@@@@@', err); console.log('err@@@@@@@@@@@@@@@@@@', err)
console.log(JSON.parse(JSON.stringify(mappedRequest))); console.log(JSON.parse(JSON.stringify(mappedRequest)))
const failure: TspProviderOriginalResponseDto = { const failure: TspProviderOriginalResponseDto = {
invoice_id: payload.id, invoice_id: payload.id,
@@ -129,21 +134,21 @@ export class NamaProviderSwitchAdapter implements IProviderSwitchAdapter {
async correctionSend( async correctionSend(
payload: TspProviderCorrectionSendPayloadDto, payload: TspProviderCorrectionSendPayloadDto,
): Promise<TspProviderCorrectionSendResponseDto> { ): Promise<TspProviderCorrectionSendResponseDto> {
const mappedRequest = this.namaProviderUtils.mapToNamaOriginalRequestDto(payload) const mappedRequest = this.namaProviderUtils.mapToNamaCorrectionDto(payload)
try { try {
const response = await this.httpClient.request( const response = await this.httpClient.request(
this.buildSendUrl(), this.buildCorrectionUrl(),
{ {
method: 'POST', method: 'POST',
body: JSON.stringify(mappedRequest), body: JSON.stringify(mappedRequest),
}, },
this.createRequestInterceptors(payload.token), this.createRequestInterceptors(payload.token),
) )
const providerResponse: NamaProviderOriginalResponseDto = await response.json() const providerResponse: NamaProviderCorrectionResponseDto = await response.json()
this.logger.debug('NAMA provider response', providerResponse) this.logger.debug('NAMA provider correction response', providerResponse)
const result: TspProviderOriginalResponseDto = { const result: TspProviderCorrectionSendResponseDto = {
invoice_id: payload.id, invoice_id: payload.id,
provider_request_payload: JSON.parse(JSON.stringify(mappedRequest)), provider_request_payload: JSON.parse(JSON.stringify(mappedRequest)),
hasError: !response.ok, hasError: !response.ok,
@@ -158,15 +163,14 @@ export class NamaProviderSwitchAdapter implements IProviderSwitchAdapter {
return result return result
} catch (err) { } catch (err) {
this.logger.error('NAMA send failed', err) this.logger.error('NAMA correction send failed', err)
const failure: TspProviderOriginalResponseDto = { const failure: TspProviderCorrectionSendResponseDto = {
invoice_id: payload.id, invoice_id: payload.id,
provider_request_payload: JSON.parse(JSON.stringify(mappedRequest)), provider_request_payload: JSON.parse(JSON.stringify(mappedRequest)),
hasError: true, hasError: true,
message: (err as Error).message, message: (err as Error).message,
received_at: new Date().toISOString(), received_at: new Date().toISOString(),
tax_id: null, status: TspProviderResponseStatus.SEND_FAILURE,
status: TspProviderResponseStatus.NOT_SEND,
} }
return failure return failure
} }
@@ -12,7 +12,7 @@ import {
PaymentInfoDto, PaymentInfoDto,
TspProviderCorrectionSendPayloadDto, TspProviderCorrectionSendPayloadDto,
TspProviderOriginalSendPayloadDto, TspProviderOriginalSendPayloadDto,
TspProviderRevokePayloadDto TspProviderRevokePayloadDto,
} from '../../dto' } from '../../dto'
import { import {
NamaProviderCorrectionRequestDto, NamaProviderCorrectionRequestDto,
@@ -76,11 +76,8 @@ export class NamaProviderUtils {
spro: '0', spro: '0',
} }
console.log('item.gold_type_payload', item.gold_type_payload);
if (item.gold_type_payload) { if (item.gold_type_payload) {
const { profit='0', commission = '0', wages = "0" } = item.gold_type_payload const { profit = '0', commission = '0', wages = '0' } = item.gold_type_payload
data = { data = {
...data, ...data,
consfee: wages, consfee: wages,
@@ -110,13 +107,18 @@ export class NamaProviderUtils {
inno: payload.invoice_number.toString(), inno: payload.invoice_number.toString(),
tins: '', tins: '',
indatim: payload.invoice_date.getTime() + '', indatim: payload.invoice_date.getTime() + '',
setm: '1', irtaxid: payload.last_tax_id,
...this.mapSettlementBased(
payload.settlement_type,
payload.payments,
payload.total_amount,
),
...this.mapCustomerInfo(payload.customer), ...this.mapCustomerInfo(payload.customer),
}, },
body: payload.items.map(item => ({ body: payload.items.map(item => {
let data: NamaProviderOriginalBodyItemDto = {
sstid: item.sku, sstid: item.sku,
vra: item.sku_vat, vra: item.sku_vat,
// sstt: item.unit_type,
fee: String(item.unit_price), fee: String(item.unit_price),
dis: String(item.discount_amount), dis: String(item.discount_amount),
mu: item.measure_unit, mu: item.measure_unit,
@@ -124,7 +126,20 @@ export class NamaProviderUtils {
consfee: '0', consfee: '0',
bros: '0', bros: '0',
spro: '0', spro: '0',
})), }
if (item.gold_type_payload) {
const { profit = '0', commission = '0', wages = '0' } = item.gold_type_payload
data = {
...data,
consfee: wages,
bros: commission,
spro: profit,
tcpbs: (Number(profit) + Number(commission) + Number(wages)).toString(),
}
}
return data
}),
} }
} }
@@ -1,9 +1,7 @@
import { CustomerType } from '@/generated/prisma/enums'
import { SaleInvoiceTspAttemptsUpdateInput } from '@/generated/prisma/models' import { SaleInvoiceTspAttemptsUpdateInput } from '@/generated/prisma/models'
import { PrismaService } from '@/prisma/prisma.service' import { PrismaService } from '@/prisma/prisma.service'
import { BadRequestException, NotFoundException } from '@nestjs/common' import { BadRequestException, NotFoundException } from '@nestjs/common'
import {
CustomerType,
} from '@/generated/prisma/enums'
import { import {
Prisma, Prisma,
TspProviderRequestType, TspProviderRequestType,
@@ -172,7 +170,6 @@ export async function buildCorrectionPayload(
invoice_date: true, invoice_date: true,
invoice_number: true, invoice_number: true,
settlement_type: true, settlement_type: true,
tax_id: true,
discount_amount: true, discount_amount: true,
tax_amount: true, tax_amount: true,
items: true, items: true,
@@ -226,16 +223,17 @@ export async function buildCorrectionPayload(
legal: true, legal: true,
}, },
}, },
reference_invoice: {
select: {
tax_id: true,
},
},
payments: { payments: {
include: { include: {
terminal_info: true, terminal_info: true,
}, },
}, },
reference_invoice: {
select: {
tax_id: true,
id: true,
},
},
}, },
}) })
@@ -284,7 +282,9 @@ export async function buildCorrectionPayload(
token: invoice.pos.complex.business_activity.partner_token, token: invoice.pos.complex.business_activity.partner_token,
tsp_provider: partner.tsp_provider!, tsp_provider: partner.tsp_provider!,
template: invoice.pos.complex.business_activity.guild.invoice_template, template: invoice.pos.complex.business_activity.guild.invoice_template,
invoice_date: new Date(), invoice_date: invoice?.invoice_date
? new Date(invoice.invoice_date)
: invoice.invoice_date,
settlement_type: invoice.settlement_type, settlement_type: invoice.settlement_type,
customer: customer:
@@ -305,11 +305,9 @@ export async function buildCorrectionPayload(
first_name: invoice.customer.individual.first_name ?? undefined, first_name: invoice.customer.individual.first_name ?? undefined,
last_name: invoice.customer.individual.last_name ?? undefined, last_name: invoice.customer.individual.last_name ?? undefined,
national_id: invoice.customer.individual.national_id ?? undefined, national_id: invoice.customer.individual.national_id ?? undefined,
mobile_number: mobile_number: invoice.customer.individual.mobile_number ?? undefined,
invoice.customer.individual.mobile_number ?? undefined,
postal_code: invoice.customer.individual.postal_code ?? undefined, postal_code: invoice.customer.individual.postal_code ?? undefined,
economic_code: economic_code: invoice.customer.individual.economic_code ?? undefined,
invoice.customer.individual.economic_code ?? undefined,
} }
: undefined, : undefined,
} }
@@ -487,11 +485,9 @@ export async function buildOriginalPayload(
first_name: invoice.customer.individual.first_name ?? undefined, first_name: invoice.customer.individual.first_name ?? undefined,
last_name: invoice.customer.individual.last_name ?? undefined, last_name: invoice.customer.individual.last_name ?? undefined,
national_id: invoice.customer.individual.national_id ?? undefined, national_id: invoice.customer.individual.national_id ?? undefined,
mobile_number: mobile_number: invoice.customer.individual.mobile_number ?? undefined,
invoice.customer.individual.mobile_number ?? undefined,
postal_code: invoice.customer.individual.postal_code ?? undefined, postal_code: invoice.customer.individual.postal_code ?? undefined,
economic_code: economic_code: invoice.customer.individual.economic_code ?? undefined,
invoice.customer.individual.economic_code ?? undefined,
} }
: undefined, : undefined,
} }
@@ -521,6 +517,62 @@ export async function buildOriginalPayload(
} }
} }
export async function getRelatedInvoiceForCorrection(
tx: Prisma.TransactionClient,
ref_invoice_id: string,
) {
const relatedInvoice = await getRelatedInvoiceForModification(tx, ref_invoice_id)
return relatedInvoice
}
export async function getRelatedInvoiceForModification(
tx: Prisma.TransactionClient,
ref_invoice_id: string,
) {
const relatedInvoice = await tx.salesInvoice.findUnique({
where: {
id: ref_invoice_id,
},
include: {
customer: {
select: {
type: true,
},
},
payments: {
select: {},
},
tsp_attempts: {
orderBy: {
attempt_no: 'desc',
},
take: 1,
},
},
})
if (!relatedInvoice) {
throw new NotFoundException('فاکتور مورد نظر یافت نشد.')
}
if (relatedInvoice.type === TspProviderRequestType.REVOKE) {
throw new BadRequestException(
'فاکتور ارسالی قبلا ابطال شده است و امکان ویرایش آن وجود ندارد.',
)
}
if (
!relatedInvoice.tax_id ||
relatedInvoice.tsp_attempts?.[0].status !== TspProviderResponseStatus.SUCCESS
) {
throw new BadRequestException(
'فاکتور قبلی همچنان در حال بررسی است و امکان اصلاح آن وجود ندارد.',
)
}
return relatedInvoice
}
export async function trySend( export async function trySend(
tspSwitchService: { tspSwitchService: {
send( send(