update return from sale invoices jurney and set nama return from sale

This commit is contained in:
2026-06-17 08:50:35 +03:30
parent f87e5b9d8e
commit 826041b07a
17 changed files with 1085 additions and 80 deletions
+6
View File
@@ -44,6 +44,12 @@ export const summarySelect: SalesInvoiceSelect = {
invoice_number: true,
},
},
referenced_by: {
select: {
id: true,
invoice_number: true,
},
},
}
export const select: SalesInvoiceSelect = {
@@ -1,13 +1,25 @@
import { SharedSaleInvoiceAccessService } from '@/common/services/saleInvoices/sale-invoice-access.service'
import { PosCorrectionSalesInvoiceDto } from '@/modules/pos/sales-invoices/dto/create-sales-invoice.dto'
import {
PosCorrectionSalesInvoiceDto,
PosReturnSalesInvoiceDto,
} from '@/modules/pos/sales-invoices/dto/create-sales-invoice.dto'
import { SalesInvoiceTspService } from '@/modules/tspProviders/sales-invoice-tsp.service'
import { Injectable } from '@nestjs/common'
import { PrismaService } from '@/prisma/prisma.service'
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'
import {
CustomerType,
TspProviderRequestType,
TspProviderResponseStatus,
} from '@prisma/client'
import { SharedSaleInvoiceCreateService } from './sale-invoice-create.service'
@Injectable()
export class SharedSaleInvoiceActionsService {
constructor(
private prisma: PrismaService,
private readonly salesInvoiceTspService: SalesInvoiceTspService,
private readonly saleInvoiceAccessService: SharedSaleInvoiceAccessService,
private sharedSaleInvoiceCreateService: SharedSaleInvoiceCreateService,
) {}
async send(consumerAccountId: string, posId: string, invoiceId: string) {
@@ -58,14 +70,464 @@ export class SharedSaleInvoiceActionsService {
consumerAccountId,
posId,
)
return this.salesInvoiceTspService.correctionSend(
const newInvoice = await this.prisma.$transaction(async tx => {
const relatedInvoice = await tx.salesInvoice.findUnique({
where: {
id: invoiceId,
pos_id: posId,
},
select: {
id: true,
invoice_date: true,
settlement_type: true,
customer_id: true,
main_id: true,
last_tsp_status: true,
referenced_by: {
select: {
id: true,
},
},
customer: {
select: {
type: true,
},
},
total_amount: true,
items: {
select: {
id: true,
good_id: true,
service_id: true,
quantity: true,
unit_price: true,
total_amount: true,
discount_amount: true,
tax_amount: true,
payload: true,
notes: true,
},
},
},
})
if (!relatedInvoice) {
throw new NotFoundException('صورت‌حساب مورد نظر شما یافت نشد.')
}
if (relatedInvoice.last_tsp_status !== TspProviderResponseStatus.SUCCESS) {
throw new BadRequestException('امکان اصلاح این صورت‌حساب وجود ندارد.')
}
if (relatedInvoice.referenced_by) {
throw new BadRequestException(
'این صورت‌حساب قبلا با یک صورت‌حساب دیگر جایگزین شده است.',
)
}
const normalizedInvoiceDate = this.normalizeInvoiceDate(data.invoice_date)
const originalInvoiceDate = this.normalizeInvoiceDate(relatedInvoice.invoice_date)
const originalItemsByKey = new Map(
relatedInvoice.items.map(item => [this.getItemKey(item), item]),
)
const usedItemKeys = new Set<string>()
const normalizedItems = data.items.map(item => {
const itemKey = this.getItemKey(item)
if (!itemKey) {
throw new BadRequestException('هر قلم باید شناسه کالا یا خدمت داشته باشد.')
}
usedItemKeys.add(itemKey)
const originalItem = originalItemsByKey.get(itemKey)
if (!originalItem) {
throw new BadRequestException(
'فقط اقلام موجود در صورت‌حساب اصلی قابل اصلاح هستند.',
)
}
const originalQuantity = Number(originalItem.quantity)
const requestedQuantity = Number(item.quantity)
const originalUnitPrice = Number(originalItem.unit_price)
const requestedUnitPrice = Number(item.unit_price)
const originalTotalAmount = this.roundAmount(Number(originalItem.total_amount))
const requestedTotalAmount = this.roundAmount(Number(item.total_amount))
if (requestedQuantity <= 0) {
throw new BadRequestException(
'تعداد/مقدار اقلام اصلاحی باید بیشتر از صفر باشد.',
)
}
// if (requestedUnitPrice < originalUnitPrice) {
// throw new BadRequestException(
// 'مبلغ واحد اقلام اصلاحی نمی‌تواند کمتر از صورت‌حساب اصلی باشد.',
// )
// }
return {
invoice_id: originalItem.id,
good_id: originalItem.good_id,
service_id: originalItem.service_id || undefined,
quantity: requestedQuantity,
unit_price: requestedUnitPrice,
total_amount: requestedTotalAmount,
discount_amount: this.roundAmount(Number(item.discount_amount || 0)),
tax_amount: this.roundAmount(Number(item.tax_amount || 0)),
payload: item.payload ? JSON.parse(JSON.stringify(item.payload)) : undefined,
notes: item.notes || undefined,
}
})
if (usedItemKeys.size !== relatedInvoice.items.length) {
throw new BadRequestException(
'فقط اقلام موجود در صورت‌حساب اصلی قابل اصلاح هستند.',
)
}
const hasChangedQuantity = normalizedItems.some(item => {
const originalItem = relatedInvoice.items.find(
relatedItem => this.getItemKey(relatedItem) === this.getItemKey(item),
)
if (!originalItem) {
return false
}
return Number(originalItem.quantity) !== Number(item.quantity)
})
const hasChangedAmount = normalizedItems.some(item => {
const originalItem = relatedInvoice.items.find(
relatedItem => this.getItemKey(relatedItem) === this.getItemKey(item),
)
if (!originalItem) {
return false
}
return (
this.roundAmount(Number(originalItem.total_amount)) !==
this.roundAmount(Number(item.total_amount))
)
})
const hasChangedInvoiceDate = normalizedInvoiceDate !== originalInvoiceDate
if (!hasChangedQuantity && !hasChangedAmount && !hasChangedInvoiceDate) {
throw new BadRequestException('مقداری تغییر نکرده است.')
}
const requestedTotalAmount = this.roundAmount(Number(data.total_amount))
const originalTotalAmount = this.roundAmount(Number(relatedInvoice.total_amount))
const totalDiff = this.roundAmount(requestedTotalAmount - originalTotalAmount)
const paymentsAmount = this.roundAmount(this.getPaymentsAmount(data.payments))
if (totalDiff > 0 && paymentsAmount !== totalDiff) {
throw new BadRequestException(
'جمع پرداختی باید برابر با اختلاف مبلغ صورت‌حساب اصلاحی و صورت‌حساب مرجع باشد.',
)
}
if (totalDiff === 0 && paymentsAmount !== 0) {
throw new BadRequestException('در صورت عدم افزایش مبلغ، پرداختی نباید ثبت شود.')
}
const newInvoice = await this.sharedSaleInvoiceCreateService.create({
tx,
data: {
invoice_date: new Date(normalizedInvoiceDate),
items: normalizedItems as any,
total_amount: requestedTotalAmount,
discount_amount: this.roundAmount(Number(data.discount_amount)),
tax_amount: this.roundAmount(Number(data.tax_amount)),
customer_id: relatedInvoice.customer_id || undefined,
customer_type: relatedInvoice.customer?.type || CustomerType.UNKNOWN,
settlement_type: relatedInvoice.settlement_type,
payments: data.payments,
} as any,
businessId,
complexId,
posId,
consumerAccountId,
main_invoice_id: relatedInvoice.main_id || relatedInvoice.id,
ref_invoice_id: relatedInvoice.id,
type: TspProviderRequestType.CORRECTION,
})
await tx.salesInvoice.update({
where: {
id: relatedInvoice.id,
},
data: {
referenced_by: {
connect: {
id: newInvoice.id,
},
},
},
})
return newInvoice
})
return this.salesInvoiceTspService.correctionSend(posId, newInvoice.id)
}
async return(
data: PosReturnSalesInvoiceDto,
consumerAccountId: string,
pos_id: string,
complex_id: string,
business_id: string,
invoiceId: string,
) {
await this.saleInvoiceAccessService.getConsumerIdWithPosAccess(
consumerAccountId,
posId,
complexId,
businessId,
invoiceId,
data,
pos_id,
)
const newInvoice = await this.prisma.$transaction(async tx => {
const relatedInvoice = await tx.salesInvoice.findUnique({
where: {
id: invoiceId,
pos_id,
},
select: {
id: true,
type: true,
invoice_date: true,
settlement_type: true,
customer_id: true,
main_id: true,
last_tsp_status: true,
referenced_by: {
select: {
id: true,
},
},
customer: {
select: {
type: true,
},
},
items: {
select: {
id: true,
good_id: true,
service_id: true,
quantity: true,
unit_price: true,
total_amount: true,
discount_amount: true,
tax_amount: true,
payload: true,
notes: true,
},
},
},
})
if (!relatedInvoice) {
throw new NotFoundException('صورت‌حساب مورد نظر شما یافت نشد.')
}
if (relatedInvoice.last_tsp_status !== TspProviderResponseStatus.SUCCESS) {
throw new BadRequestException('امکان برگشت از خرید روی این صورت‌حساب وجود ندارد.')
}
if (relatedInvoice.referenced_by) {
throw new BadRequestException(
'این صورت‌حساب قبلا با یک صورت‌حساب دیگر جایگزین شده است.',
)
}
const normalizedInvoiceDate = this.normalizeInvoiceDate(data.invoice_date)
const originalInvoiceDate = this.normalizeInvoiceDate(relatedInvoice.invoice_date)
const originalItemsByKey = new Map(
relatedInvoice.items.map(item => [this.getReturnItemKey(item), item]),
)
const usedItemKeys = new Set<string>()
const normalizedItems = data.items.map(item => {
const itemKey = this.getReturnItemKey(item)
if (!itemKey) {
throw new BadRequestException('هر قلم باید شناسه کالا یا خدمت داشته باشد.')
}
// if (usedItemKeys.has(itemKey)) {
// throw new BadRequestException('اقلام تکراری در صورت‌حساب بازگشتی مجاز نیستند.')
// }
usedItemKeys.add(itemKey)
const originalItem = originalItemsByKey.get(itemKey)
if (!originalItem) {
throw new BadRequestException(
'فقط اقلام موجود در صورت‌حساب اصلی قابل بازگشت هستند.',
)
}
const originalQuantity = Number(originalItem.quantity)
const requestedQuantity = Number(item.quantity)
if (requestedQuantity <= 0) {
throw new BadRequestException(
'تعداد/مقدار اقلام بازگشتی باید بیشتر از صفر باشد.',
)
}
if (requestedQuantity > originalQuantity) {
throw new BadRequestException(
'تعداد/مقدار اقلام بازگشتی نمی‌تواند از مقدار صورت‌حساب اصلی بیشتر باشد.',
)
}
const quantityRatio = requestedQuantity / originalQuantity
return {
invoice_id: originalItem.id,
good_id: originalItem.good_id,
service_id: originalItem.service_id || undefined,
quantity: requestedQuantity,
unit_price: Number(originalItem.unit_price),
total_amount: this.roundAmount(
Number(originalItem.total_amount) * quantityRatio,
),
discount_amount: this.roundAmount(
Number(originalItem.discount_amount || 0) * quantityRatio,
),
tax_amount: this.roundAmount(
Number(originalItem.tax_amount || 0) * quantityRatio,
),
payload: originalItem.payload
? JSON.parse(JSON.stringify(originalItem.payload))
: undefined,
notes: originalItem.notes || undefined,
}
})
if (!normalizedItems.length) {
throw new BadRequestException(
'حداقل یک قلم باید در صورت‌حساب بازگشتی باقی بماند.',
)
}
const hasRemovedItem = relatedInvoice.items.some(
item => !usedItemKeys.has(this.getReturnItemKey(item)),
)
const hasChangedQuantity = normalizedItems.some(item => {
const originalItem = relatedInvoice.items.find(
relatedItem =>
this.getReturnItemKey(relatedItem) === this.getReturnItemKey(item),
)
if (!originalItem) {
return false
}
return Number(originalItem.quantity) !== Number(item.quantity)
})
const hasChangedInvoiceDate = normalizedInvoiceDate !== originalInvoiceDate
if (!hasRemovedItem && !hasChangedQuantity && !hasChangedInvoiceDate) {
throw new BadRequestException('مقداری تغییر نکرده است.')
}
const totalAmount = normalizedItems.reduce(
(sum, item) => sum + Number(item.total_amount),
0,
)
const discountAmount = normalizedItems.reduce(
(sum, item) => sum + Number(item.discount_amount || 0),
0,
)
const taxAmount = normalizedItems.reduce(
(sum, item) => sum + Number(item.tax_amount || 0),
0,
)
const newInvoice = await this.sharedSaleInvoiceCreateService.create({
tx,
data: {
invoice_date: new Date(normalizedInvoiceDate),
items: normalizedItems as any,
total_amount: totalAmount,
discount_amount: discountAmount,
tax_amount: taxAmount,
customer_id: relatedInvoice.customer_id || undefined,
customer_type: relatedInvoice.customer?.type || CustomerType.UNKNOWN,
settlement_type: relatedInvoice.settlement_type,
payments: undefined,
} as any,
businessId: business_id,
complexId: complex_id,
posId: pos_id,
consumerAccountId,
main_invoice_id: relatedInvoice.main_id || relatedInvoice.id,
ref_invoice_id: relatedInvoice.id,
type: TspProviderRequestType.RETURN,
})
await tx.salesInvoice.update({
where: {
id: relatedInvoice.id,
},
data: {
referenced_by: {
connect: {
id: newInvoice.id,
},
},
},
})
return newInvoice
})
return this.salesInvoiceTspService.returnFromSaleSend(
pos_id,
business_id,
newInvoice.id,
)
}
private normalizeInvoiceDate(invoiceDate: Date | string) {
return new Date(invoiceDate).toISOString()
}
private getItemKey(item: { good_id?: string | null; service_id?: string | null }) {
return item.good_id || item.service_id || ''
}
private getPaymentsAmount(payments?: {
terminals?: { amount?: number }
cash?: number
set_off?: number
card?: number
bank?: number
check?: number
other?: number
}) {
return (
Number(payments?.terminals?.amount || 0) +
Number(payments?.cash || 0) +
Number(payments?.set_off || 0) +
Number(payments?.card || 0) +
Number(payments?.bank || 0) +
Number(payments?.check || 0) +
Number(payments?.other || 0)
)
}
private roundAmount(amount: number) {
return Number(amount.toFixed(2))
}
async inquiry(consumerAccountId: string, posId: string, invoiceId: string) {
@@ -75,4 +537,11 @@ export class SharedSaleInvoiceActionsService {
)
return this.salesInvoiceTspService.get(invoiceId, posId, consumerId)
}
private getReturnItemKey(item: {
good_id?: string | null
service_id?: string | null
}) {
return item.good_id || item.service_id || ''
}
}
@@ -196,6 +196,11 @@ export class SharedCreateSalesInvoiceDto {
@ValidateNested({ each: true })
items: SharedCreateSalesInvoiceItemDto[]
@ApiProperty({ required: false })
@IsOptional()
@IsString()
ref_invoice_id?: string
@ApiProperty({ required: false })
@IsOptional()
@IsString()
@@ -230,5 +235,13 @@ export class SharedCreateSalesInvoiceDto {
export class SharedCorrectionSalesInvoiceDto extends OmitType(
SharedCreateSalesInvoiceDto,
['customer', 'customer_id', 'send_to_tsp', 'customer_type', 'settlement_type'],
['customer', 'customer_id', 'customer_type', 'settlement_type'],
) {}
export class SharedReturnSalesInvoiceDto extends OmitType(SharedCreateSalesInvoiceDto, [
'customer',
'customer_id',
'customer_type',
'settlement_type',
'payments',
]) {}
@@ -58,7 +58,10 @@ export class SharedSaleInvoiceCreateService {
} = input
const normalizedInvoiceDate = this.normalizeInvoiceDate(data.invoice_date)
const payments = this.buildPaymentsData(data.payments, data.total_amount)
const payments =
type === TspProviderRequestType.ORIGINAL || data.payments
? this.buildPaymentsData(data.payments, data.total_amount)
: []
for (let attempt = 1; attempt <= this.createInvoiceRetries; attempt++) {
try {
@@ -86,7 +89,14 @@ export class SharedSaleInvoiceCreateService {
select: { ...QUERY_CONSTANTS.SALE_INVOICE.select },
})
await this.createPayments($tx, salesInvoice.id, payments, normalizedInvoiceDate)
if (payments.length) {
await this.createPayments(
$tx,
salesInvoice.id,
payments,
normalizedInvoiceDate,
)
}
return salesInvoice
})
@@ -468,7 +478,7 @@ export class SharedSaleInvoiceCreateService {
return salesInvoiceData
}
private async getNextInvoiceNumber(tx: Prisma.TransactionClient, businessId: string) {
async getNextInvoiceNumber(tx: Prisma.TransactionClient, businessId: string) {
const latestInvoice = await tx.salesInvoice.findFirst({
where: {
pos: {
@@ -525,17 +535,26 @@ export class SharedSaleInvoiceCreateService {
})
if (payment.method === PaymentMethodType.TERMINAL && payment.terminalInfo) {
const {
terminal_id,
stan,
rrn,
transaction_date_time,
customer_card_no,
description,
} = payment.terminalInfo
await tx.salesInvoicePaymentTerminalInfo.create({
data: {
payment_id: createdPayment.id,
terminal_id: payment.terminalInfo.terminal_id,
stan: payment.terminalInfo.stan,
rrn: payment.terminalInfo.rrn,
transaction_date_time: new Date(
payment.terminalInfo.transaction_date_time || '',
),
customer_card_no: payment.terminalInfo.customer_card_no || null,
description: payment.terminalInfo.description || null,
terminal_id,
stan: stan,
rrn: rrn,
transaction_date_time: transaction_date_time
? new Date(transaction_date_time)
: new Date(),
customer_card_no: '1234567890123456',
// customer_card_no: customer_card_no || null,
description: description || null,
},
})
}
@@ -2,6 +2,51 @@ import {
SharedCorrectionSalesInvoiceDto,
SharedCreateSalesInvoiceDto,
} from '@/common/services/saleInvoices/sale-invoice-create.dto'
import { ApiProperty } from '@nestjs/swagger'
import { Type } from 'class-transformer'
import {
ArrayMinSize,
IsArray,
IsDateString,
IsNumber,
IsOptional,
IsString,
Min,
ValidateNested,
} from 'class-validator'
export class PosCreateSalesInvoiceDto extends SharedCreateSalesInvoiceDto {}
export class PosCorrectionSalesInvoiceDto extends SharedCorrectionSalesInvoiceDto {}
export class PosReturnSalesInvoiceItemDto {
@ApiProperty({ required: false })
@IsOptional()
@IsString()
good_id?: string
@ApiProperty({ required: false })
@IsOptional()
@IsString()
service_id?: string
@ApiProperty({ required: true, default: 1 })
@IsNumber()
@Min(1)
quantity: number
}
export class PosReturnSalesInvoiceDto {
@ApiProperty({ required: true })
@IsDateString(
{ strict: true },
{ message: 'invoice_date must be a valid ISO-8601 string' },
)
invoice_date: Date
@ApiProperty({ required: true, type: [PosReturnSalesInvoiceItemDto] })
@IsArray()
@ArrayMinSize(1)
@ValidateNested({ each: true })
@Type(() => PosReturnSalesInvoiceItemDto)
items: PosReturnSalesInvoiceItemDto[]
}
@@ -5,6 +5,7 @@ import type { IPosPayload } from '@/common/models/posPayload.model'
import {
PosCorrectionSalesInvoiceDto,
PosCreateSalesInvoiceDto,
PosReturnSalesInvoiceDto,
} from './dto/create-sales-invoice.dto'
import { SalesInvoicesFilterDto } from './dto/sales-invoices-filter.dto'
import { SalesInvoicesService } from './sales-invoices.service'
@@ -60,9 +61,13 @@ export class SalesInvoicesController {
) {
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(':id/return_from_sale')
return(
@Param('id') id: string,
@PosInfo() posInfo: IPosPayload,
@Body() data: PosReturnSalesInvoiceDto,
) {
return this.salesInvoicesService.return(id, posInfo, data)
}
// @Post('send/bulk')
@@ -13,6 +13,7 @@ import { SalesInvoiceTspService } from '../../tspProviders/sales-invoice-tsp.ser
import {
PosCorrectionSalesInvoiceDto,
PosCreateSalesInvoiceDto,
PosReturnSalesInvoiceDto,
} from './dto/create-sales-invoice.dto'
import { SalesInvoicesFilterDto } from './dto/sales-invoices-filter.dto'
@@ -172,10 +173,11 @@ export class SalesInvoicesService {
return ResponseMapper.single(invoice)
}
async return(invoiceId: string, posInfo: IPosPayload) {
async return(invoiceId: string, posInfo: IPosPayload, data: PosReturnSalesInvoiceDto) {
const { consumer_account_id, pos_id, business_id, complex_id } = posInfo
const invoice = await this.sharedSaleInvoiceActionsService.revoke(
const invoice = await this.sharedSaleInvoiceActionsService.return(
data,
consumer_account_id,
pos_id,
complex_id,
@@ -183,7 +185,7 @@ export class SalesInvoicesService {
invoiceId,
)
return ResponseMapper.single(invoice)
return ResponseMapper.create(invoice)
}
async inquiry(consumer_account_id: string, pos_id: string, invoiceId: string) {
@@ -46,8 +46,9 @@ export class TspProviderCorrectionInvoicePayloadDto {
@IsObject()
@ValidateNested({ each: true })
@Type(() => SharedCreateSalesInvoicePaymentsDto)
payments: SharedCreateSalesInvoicePaymentsDto
payments?: SharedCreateSalesInvoicePaymentsDto
}
export class TspProviderCorrectionSendPayloadDto extends TspProviderOriginalSendPayloadDto {
@ApiProperty({ required: true })
@IsString()
+1
View File
@@ -3,4 +3,5 @@ export * from './correction.dto'
export * from './get.dto'
export * from './original.dto'
export * from './provider-switch.dto'
export * from './return.dto'
export * from './revoke.dto'
@@ -0,0 +1,48 @@
import { SharedCreateSalesInvoiceItemDto } from '@/common/services/saleInvoices/sale-invoice-create.dto'
import { ApiProperty } from '@nestjs/swagger'
import { Type } from 'class-transformer'
import {
ArrayMinSize,
IsArray,
IsDateString,
IsNumber,
IsString,
ValidateNested,
} from 'class-validator'
import {
TspProviderOriginalResponseDto,
TspProviderOriginalSendPayloadDto,
} from './original.dto'
export class TspProviderReturnInvoicePayloadDto {
@ApiProperty({ required: true, nullable: true })
@IsDateString()
invoice_date: Date
@ApiProperty({ type: [SharedCreateSalesInvoiceItemDto] })
@IsArray()
@ValidateNested({ each: true })
@Type(() => SharedCreateSalesInvoiceItemDto)
@ArrayMinSize(1)
items: SharedCreateSalesInvoiceItemDto[]
@ApiProperty()
@IsNumber()
total_amount: number
@ApiProperty()
@IsNumber()
discount_amount: number
@ApiProperty()
@IsNumber()
tax_amount: number
}
export class TspProviderReturnSendPayloadDto extends TspProviderOriginalSendPayloadDto {
@ApiProperty({ required: true })
@IsString()
last_tax_id: string
}
export class TspProviderReturnSendResponseDto extends TspProviderOriginalResponseDto {}
@@ -6,6 +6,8 @@ import {
TspProviderGetResponseDto,
TspProviderOriginalResponseDto,
TspProviderOriginalSendPayloadDto,
TspProviderReturnSendPayloadDto,
TspProviderReturnSendResponseDto,
TspProviderRevokePayloadDto,
TspProviderRevokeResponseDto,
} from './dto'
@@ -48,6 +50,14 @@ export class SalesInvoiceTspSwitchService {
return adapter.correctionSend(payload)
}
async returnFromSale(
payload: TspProviderReturnSendPayloadDto,
): Promise<TspProviderReturnSendResponseDto> {
const adapter = this.resolveSwitch(payload.tsp_provider)
return adapter.returnSend(payload)
}
async sendBulk(
payloads: TspProviderOriginalSendPayloadDto[],
): Promise<TspProviderBulkSendResultDto[]> {
@@ -6,17 +6,14 @@ import {
TspProviderRequestType,
TspProviderResponseStatus,
} from 'generated/prisma/client'
import {
TspProviderActionResponseDto,
TspProviderCorrectionInvoicePayloadDto,
} from './dto'
import { TspProviderActionResponseDto } from './dto'
import { SalesInvoiceTspSwitchService } from './sales-invoice-tsp-switch.service'
import {
buildCorrectionPayload,
buildOriginalPayload,
buildReturnFromSalePayload,
buildRevokePayload,
getOriginalResendAttemptNumber,
getRelatedInvoiceForCorrection,
onResult,
} from './utils/sales-invoice-tsp.utils'
@@ -176,60 +173,68 @@ export class SalesInvoiceTspService {
}
async correctionSend(
consumerAccountId: string,
posId: string,
complexId: string,
businessId: string,
ref_invoice_id: string,
dataToUpdate: TspProviderCorrectionInvoicePayloadDto,
invoice_id: string,
): Promise<TspProviderActionResponseDto> {
const [newInvoice, attempt, correctionPayload] = await this.prisma.$transaction(
async tx => {
const relatedInvoice = await getRelatedInvoiceForCorrection(tx, ref_invoice_id)
const payload = await buildCorrectionPayload(this.prisma, invoice_id, posId)
const newInvoice = await this.sharedSaleInvoiceCreateService.create({
tx,
data: {
customer_type: relatedInvoice.customer?.type || CustomerType.UNKNOWN,
invoice_date: dataToUpdate.invoice_date,
payments: dataToUpdate.payments,
items: dataToUpdate.items,
total_amount: dataToUpdate.total_amount,
discount_amount: dataToUpdate.discount_amount,
tax_amount: dataToUpdate.tax_amount,
customer_id: relatedInvoice.customer_id || undefined,
settlement_type: relatedInvoice.settlement_type,
},
businessId,
complexId,
posId,
consumerAccountId,
main_invoice_id: relatedInvoice.main_id || relatedInvoice.id,
ref_invoice_id: relatedInvoice.id,
type: TspProviderRequestType.CORRECTION,
})
const attemptNumber = 1
const correctionPayload = await buildCorrectionPayload(tx, newInvoice.id)
const attempt = await tx.saleInvoiceTspAttempts.create({
data: {
attempt_no: 1,
invoice_id: newInvoice.id,
message: 'در حال ارسال به سامانه مالیاتی...',
status: TspProviderResponseStatus.QUEUED,
sent_at: new Date().toISOString(),
raw_request_payload: JSON.parse(JSON.stringify(correctionPayload)),
provider_request_payload: {},
},
})
return [newInvoice, attempt, correctionPayload]
const invoice = await this.prisma.salesInvoice.update({
where: {
id: invoice_id,
},
)
data: {
last_tsp_status: TspProviderResponseStatus.QUEUED,
last_attempt_no: attemptNumber,
type: TspProviderRequestType.CORRECTION,
tsp_attempts: {
create: {
attempt_no: attemptNumber,
provider_request_payload: {},
status: TspProviderResponseStatus.QUEUED,
raw_request_payload: JSON.parse(JSON.stringify(payload)),
sent_at: new Date().toISOString(),
message: 'در حال ارسال به سامانه مالیاتی...',
},
},
},
})
const result = await this.tspSwitchService.correction(correctionPayload)
const result = await this.tspSwitchService.send(payload)
return await onResult(this.prisma, result, invoice.id)
}
return await onResult(this.prisma, result, newInvoice.id)
async returnFromSaleSend(
pos_id: string,
business_id: string,
invoice_id: string,
): Promise<TspProviderActionResponseDto> {
const payload = await buildReturnFromSalePayload(this.prisma, invoice_id, pos_id)
const attemptNumber = 1
const invoice = await this.prisma.salesInvoice.update({
where: {
id: invoice_id,
},
data: {
last_tsp_status: TspProviderResponseStatus.QUEUED,
last_attempt_no: attemptNumber,
tsp_attempts: {
create: {
attempt_no: attemptNumber,
provider_request_payload: {},
status: TspProviderResponseStatus.QUEUED,
raw_request_payload: JSON.parse(JSON.stringify(payload)),
sent_at: new Date().toISOString(),
message: 'در حال ارسال به سامانه مالیاتی...',
},
},
},
})
const result = await this.tspSwitchService.returnFromSale(payload)
return await onResult(this.prisma, result, invoice.id)
}
async revoke(
@@ -2,4 +2,5 @@ export * from './common.dto'
export * from './correction.dto'
export * from './get.dto'
export * from './original.dto'
export * from './returnFromSale.dto'
export * from './revoke.dto'
@@ -0,0 +1,60 @@
import { ApiProperty } from '@nestjs/swagger'
import { Type } from 'class-transformer'
import { IsArray, IsString, Length, ValidateNested } from 'class-validator'
import {
NamaProviderCommonBodyDto,
NamaProviderCommonHeaderDto,
NamaProviderPaymentInfoDto,
NamaProviderResponseDto,
} from './common.dto'
export class NamaProviderReturnHeaderDto extends NamaProviderCommonHeaderDto {
@ApiProperty({ required: true, description: 'موضوع صورتحساب (3- برگشت از فروش)' })
@IsString()
ins: '3'
@ApiProperty({
required: true,
description: 'شناسه مالیاتی صورت‌حساب مرجع',
minLength: 22,
maxLength: 22,
})
@IsString()
@Length(22, 22)
irtaxid: string
}
export class NamaProviderReturnBodyItemDto extends NamaProviderCommonBodyDto {}
export class NamaProviderReturnRequestDto {
@ApiProperty({ type: [NamaProviderReturnBodyItemDto] })
@IsArray()
@ValidateNested({ each: true })
@Type(() => NamaProviderReturnBodyItemDto)
body: NamaProviderReturnBodyItemDto[]
@ApiProperty({ type: [NamaProviderPaymentInfoDto] })
@IsArray()
@ValidateNested({ each: true })
@Type(() => NamaProviderPaymentInfoDto)
payment: NamaProviderPaymentInfoDto[]
@ApiProperty({ type: NamaProviderReturnHeaderDto })
@ValidateNested({ each: true })
@Type(() => NamaProviderReturnHeaderDto)
header: NamaProviderReturnHeaderDto
@ApiProperty()
@IsString()
uuid: string
@ApiProperty()
@IsString()
economic_code: string
@ApiProperty()
@IsString()
fiscal_id: string
}
export class NamaProviderReturnResponseDto extends NamaProviderResponseDto {}
@@ -12,6 +12,8 @@ import {
TspProviderGetResponseDto,
TspProviderOriginalResponseDto,
TspProviderOriginalSendPayloadDto,
TspProviderReturnSendPayloadDto,
TspProviderReturnSendResponseDto,
TspProviderRevokePayloadDto,
TspProviderRevokeResponseDto,
} from '../../dto'
@@ -27,6 +29,7 @@ import {
NamaProviderRevokeRequestDto,
NamaProviderRevokeResponseDto,
} from './dto'
import { NamaProviderReturnResponseDto } from './dto/returnFromSale.dto'
import { NamaProviderUtils } from './nama-provider.util'
@Injectable()
@@ -40,6 +43,7 @@ export class NamaProviderSwitchAdapter implements IProviderSwitchAdapter {
private readonly sendPath = '/api/v1/invoices/single-send'
private readonly correctionPath = this.sendPath
private readonly returnFromSalePath = this.sendPath
private readonly sendBulkPath = '/api/v1/invoices'
private readonly checkBulkStatus = '/api/v1/invoices/check-by-uuids'
private readonly getPath = '/api/v1/invoices'
@@ -63,6 +67,10 @@ export class NamaProviderSwitchAdapter implements IProviderSwitchAdapter {
return `${this.baseUrl}${this.correctionPath}`
}
private buildReturnFromSaleUrl() {
return `${this.baseUrl}${this.returnFromSalePath}`
}
private buildGetUrl(invoiceId: string) {
return `${this.baseUrl}${this.getPath}/${invoiceId}`
}
@@ -176,6 +184,51 @@ export class NamaProviderSwitchAdapter implements IProviderSwitchAdapter {
}
}
async returnSend(
payload: TspProviderReturnSendPayloadDto,
): Promise<TspProviderReturnSendResponseDto> {
const mappedRequest = this.namaProviderUtils.mapToNamaReturnDto(payload)
try {
const response = await this.httpClient.request(
this.buildReturnFromSaleUrl(),
{
method: 'POST',
body: JSON.stringify(mappedRequest),
},
this.createRequestInterceptors(payload.token),
)
const providerResponse: NamaProviderReturnResponseDto = await response.json()
this.logger.debug('NAMA provider return response', providerResponse)
const result: TspProviderReturnSendResponseDto = {
invoice_id: payload.id,
provider_request_payload: JSON.parse(JSON.stringify(mappedRequest)),
hasError: !response.ok,
message: providerResponse.message,
provider_response: JSON.parse(JSON.stringify(providerResponse)),
received_at: new Date().toISOString(),
tax_id: providerResponse.tax_id,
status: this.namaProviderUtils.mapResponseStatus(
providerResponse.status as NamaProviderResponseStatus,
),
}
return result
} catch (err) {
this.logger.error('NAMA return from sale send failed', err)
const failure: TspProviderReturnSendResponseDto = {
invoice_id: payload.id,
provider_request_payload: JSON.parse(JSON.stringify(mappedRequest)),
hasError: true,
message: (err as Error).message,
received_at: new Date().toISOString(),
status: TspProviderResponseStatus.SEND_FAILURE,
}
return failure
}
}
async sendBulk(
payloads: TspProviderOriginalSendPayloadDto[],
): Promise<TspProviderBulkSendResultDto[]> {
@@ -12,6 +12,7 @@ import {
PaymentInfoDto,
TspProviderCorrectionSendPayloadDto,
TspProviderOriginalSendPayloadDto,
TspProviderReturnSendPayloadDto,
TspProviderRevokePayloadDto,
} from '../../dto'
import {
@@ -22,6 +23,7 @@ import {
NamaProviderResponseStatus,
NamaProviderRevokeRequestDto,
} from './dto'
import { NamaProviderReturnRequestDto } from './dto/returnFromSale.dto'
export class NamaProviderUtils {
mapResponseStatus(status: NamaProviderResponseStatus): TspProviderResponseStatus {
@@ -143,6 +145,57 @@ export class NamaProviderUtils {
}
}
mapToNamaReturnDto(
payload: TspProviderReturnSendPayloadDto,
): NamaProviderReturnRequestDto {
return {
uuid: payload.id,
economic_code: payload.economic_code,
fiscal_id: payload.fiscal_id,
payment: this.mapPayments(payload.payments),
header: {
ins: this.mapRequestType(TspProviderRequestType.RETURN),
inp: this.mapInvoiceTemplate(payload.template),
inty: this.mapTspProviderCustomerType(payload.customer?.type),
inno: payload.invoice_number.toString(),
tins: '',
indatim: payload.invoice_date.getTime() + '',
irtaxid: payload.last_tax_id,
...this.mapSettlementBased(
payload.settlement_type,
payload.payments,
payload.total_amount,
),
...this.mapCustomerInfo(payload.customer),
},
body: payload.items.map(item => {
let data: NamaProviderOriginalBodyItemDto = {
sstid: item.sku,
vra: item.sku_vat,
fee: String(item.unit_price),
dis: String(parseInt(item.discount_amount + '')),
mu: item.measure_unit,
am: String(item.quantity),
consfee: '0',
bros: '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
}),
}
}
mapToNamaRevokeRequestDto(
payload: TspProviderRevokePayloadDto,
): NamaProviderRevokeRequestDto {
@@ -9,12 +9,13 @@ import {
} from 'generated/prisma/client'
import {
goldTypePayload,
TspProviderCorrectionSendPayloadDto,
TspProviderGetResponseDto,
TspProviderOriginalResponseDto,
TspProviderOriginalSendPayloadDto,
TspProviderReturnSendPayloadDto,
TspProviderRevokePayloadDto,
} from '../dto'
import { TspProviderCorrectionSendPayloadDto } from '../dto/correction.dto'
import { TspProviderActionResponseDto } from '../dto/provider-switch.dto'
export async function getOriginalResendAttemptNumber(
@@ -160,10 +161,12 @@ export async function buildRevokePayload(
export async function buildCorrectionPayload(
tx: Prisma.TransactionClient,
invoice_id: string,
pos_id: string,
): Promise<TspProviderCorrectionSendPayloadDto> {
const invoice = await tx.salesInvoice.findUnique({
where: {
id: invoice_id,
pos_id,
},
select: {
id: true,
@@ -323,6 +326,217 @@ export async function buildCorrectionPayload(
}
}
export async function buildReturnFromSalePayload(
prisma: PrismaService,
invoiceId: string,
posId: string,
): Promise<TspProviderReturnSendPayloadDto> {
const invoice = await prisma.salesInvoice.findUnique({
where: {
id: invoiceId,
pos_id: posId,
},
select: {
id: true,
code: true,
total_amount: true,
invoice_date: true,
invoice_number: true,
settlement_type: true,
discount_amount: true,
tax_amount: true,
tax_id: true,
items: {
select: {
id: true,
quantity: true,
unit_price: true,
total_amount: true,
measure_unit_code: true,
measure_unit_text: true,
sku_code: true,
sku_vat: true,
good_id: true,
service_id: true,
discount_amount: true,
tax_amount: true,
payload: true,
good_snapshot: 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,
},
},
},
},
},
},
},
},
},
},
},
},
unknown_customer: true,
customer: {
select: {
type: true,
individual: true,
legal: true,
},
},
payments: {
select: {
amount: true,
payment_method: true,
paid_at: true,
terminal_info: {
select: {
stan: true,
rrn: true,
transaction_date_time: true,
customer_card_no: true,
terminal_id: true,
},
},
},
},
reference_invoice: {
select: {
tax_id: true,
},
},
},
})
if (!invoice) {
throw new NotFoundException('صورت‌حساب مورد نظر یافت نشد.')
}
if (!invoice.reference_invoice) {
throw new NotFoundException('صورت‌حساب مرجع برای برگشت از فروش یافت نشد.')
}
const {
pos,
id,
invoice_number,
invoice_date,
total_amount,
discount_amount,
tax_amount,
settlement_type,
} = invoice
const { business_activity: ba } = pos.complex
const unknown_customer = (invoice.unknown_customer || {}) as Record<string, string>
const { partner } = (ba.consumer.legal || ba.consumer.individual)!
return {
id,
invoice_number,
invoice_date,
settlement_type,
total_amount: Number(total_amount),
tax_amount: Number(tax_amount),
discount_amount: Number(discount_amount),
economic_code: ba.economic_code,
fiscal_id: ba.fiscal_id,
template: ba.guild.invoice_template,
token: ba.partner_token,
tsp_provider: partner.tsp_provider!,
last_tax_id: invoice.reference_invoice.tax_id!,
payments: invoice.payments.map(payment => ({
amount: Number(payment.amount),
payment_method: payment.payment_method,
paid_at: payment.paid_at,
terminal_info: {
card_number: payment.terminal_info?.customer_card_no
? payment.terminal_info.customer_card_no
: undefined,
tracking_code: payment.terminal_info ? payment.terminal_info.rrn : undefined,
},
})),
customer:
invoice.customer && invoice.customer.type !== CustomerType.UNKNOWN
? {
type: invoice.customer.type,
legal_info: invoice.customer.legal
? {
name: invoice.customer.legal.name ?? undefined,
registration_number:
invoice.customer.legal.registration_number ?? undefined,
postal_code: invoice.customer.legal.postal_code ?? undefined,
economic_code: invoice.customer.legal.economic_code ?? undefined,
}
: undefined,
individual_info: invoice.customer.individual
? {
first_name: invoice.customer.individual.first_name ?? undefined,
last_name: invoice.customer.individual.last_name ?? undefined,
national_id: invoice.customer.individual.national_id ?? undefined,
mobile_number: invoice.customer.individual.mobile_number ?? undefined,
postal_code: invoice.customer.individual.postal_code ?? undefined,
economic_code: invoice.customer.individual.economic_code ?? undefined,
}
: undefined,
}
: {
type: CustomerType.UNKNOWN,
unknown_info: {
name: unknown_customer?.name || '',
economic_code: unknown_customer?.economic_code || '',
},
},
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),
tax_amount: Number(tax_amount),
discount_amount: Number(discount_amount),
measure_unit: item.measure_unit_code,
sku: item.sku_code,
sku_vat: String(item.sku_vat),
discount: String((item.payload as Record<string, any>)?.discount || '0'),
good_id: item.good_id,
service_id: item.service_id,
good_snapshot: item.good_snapshot,
gold_type_payload: isGoldTypePayload(item.payload) ? item.payload : undefined,
})),
}
}
export async function buildOriginalPayload(
prisma: PrismaService,
invoiceId: string,