Refactor: Remove stored procedures and replace with direct database operations

feat: Add economic code and guild information to sales invoice selection

fix: Update error messages for invoice access and creation to use "صورت‌حساب"

fix: Change error messages in SaleInvoicesService to use "صورت‌حساب" instead of "فاکتور"

fix: Update error messages in SalesInvoicesService for not found cases to use "صورت‌حساب"

fix: Modify TSP service to handle invoice updates and error messages consistently with "صورت‌حساب"

fix: Update common DTO descriptions to refer to "صورت‌حساب" instead of "فاکتور"

fix: Adjust utility functions to handle invoice references and error messages with "صورت‌حساب"
This commit is contained in:
2026-06-11 16:13:17 +03:30
parent 47a27fb54f
commit 23bfe1ecbe
12 changed files with 114 additions and 2105 deletions
+6
View File
@@ -73,6 +73,12 @@ export const select: SalesInvoiceSelect = {
select: {
id: true,
name: true,
economic_code: true,
guild: {
select: {
name: true,
},
},
},
},
},
@@ -21,7 +21,7 @@ export class SharedSaleInvoiceAccessService {
})
if (!consumer) {
throw new BadRequestException('شما دسترسی لازم برای ارسال فاکتور را ندارید.')
throw new BadRequestException('شما دسترسی لازم برای ارسال صورت‌حساب را ندارید.')
}
return consumer.consumer_id
@@ -109,7 +109,7 @@ export class SharedSaleInvoiceCreateService {
}
}
throw new BadRequestException('ایجاد فاکتور با خطا مواجه شد.')
throw new BadRequestException('ایجاد صورت‌حساب با خطا مواجه شد.')
}
private isRetryableInvoiceConflict(error: unknown) {
@@ -197,7 +197,7 @@ export class SharedSaleInvoiceCreateService {
const roundedTotalAmount = Number(Number(totalAmount).toFixed(2))
if (roundedTotalPayments !== roundedTotalAmount) {
throw new BadRequestException('مبلغ پرداختی باید برابر با مبلغ کل فاکتور باشد.')
throw new BadRequestException('مبلغ پرداختی باید برابر با مبلغ کل صورت‌حساب باشد.')
}
const terminalPayments = payments.filter(
@@ -431,7 +431,7 @@ export class SharedSaleInvoiceCreateService {
type !== TspProviderRequestType.ORIGINAL &&
!(main_invoice_id || ref_invoice_id)
) {
throw new BadRequestException('متاسفانه مشکلی در اطلاعات فاکتور وجود دارد.')
throw new BadRequestException('متاسفانه مشکلی در اطلاعات صورت‌حساب وجود دارد.')
}
const salesInvoiceData: SalesInvoiceCreateInput = {
@@ -253,7 +253,7 @@ export class SaleInvoicesService {
if (invoice) {
return ResponseMapper.single(this.invoiceMapper(invoice))
}
throw new NotFoundException('فاکتور مورد نظر شما یافت نشد.')
throw new NotFoundException('صورت‌حساب مورد نظر شما یافت نشد.')
}
async send(invoiceId: string, posInfo: IPosPayload) {
@@ -301,7 +301,7 @@ export class SaleInvoicesService {
async sendBulk(consumer_id: string, invoiceIds: string[]) {
if (!invoiceIds.length) {
throw new BadRequestException('لیست شناسه فاکتورها نمی‌تواند خالی باشد.')
throw new BadRequestException('لیست شناسه ‌صورت‌حساب‌ها نمی‌تواند خالی باشد.')
}
await this.salesInvoiceTaxService.sendBulk(consumer_id, invoiceIds)
@@ -91,7 +91,7 @@ export class SalesInvoicesService {
}
return ResponseMapper.single(mappedInvoice)
} else throw new NotFoundException('فاکتور مورد نظر شما یافت نشد.')
} else throw new NotFoundException('صورت‌حساب مورد نظر شما یافت نشد.')
}
async create(data: PosCreateSalesInvoiceDto, posInfo: IPosPayload) {
@@ -42,20 +42,28 @@ export class SalesInvoiceTspService {
const attemptNumber = await getOriginalResendAttemptNumber(this.prisma, invoice_id)
const attempt = await this.prisma.saleInvoiceTspAttempts.create({
const invoice = await this.prisma.salesInvoice.update({
where: {
id: invoice_id,
},
data: {
attempt_no: attemptNumber,
invoice_id,
provider_request_payload: {},
status: TspProviderResponseStatus.QUEUED,
raw_request_payload: JSON.parse(JSON.stringify(payload)),
sent_at: new Date().toISOString(),
message: 'در حال ارسال به سامانه مالیاتی...',
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.send(payload)
return await onResult(this.prisma, result, attempt.id)
return await onResult(this.prisma, result, invoice.id)
}
async sendBulk(consumer_id: string, invoice_ids: string[]): Promise<void> {
@@ -75,18 +83,17 @@ export class SalesInvoiceTspService {
pos_id: string,
consumer_id: string,
): Promise<TspProviderActionResponseDto> {
const [attempt, pos] = await this.prisma.$transaction(async tx => [
await tx.saleInvoiceTspAttempts.findFirst({
const [invoice, pos] = await this.prisma.$transaction(async tx => [
await tx.salesInvoice.findUnique({
where: {
invoice_id,
invoice: {
pos: {
id: pos_id,
},
id: invoice_id,
pos: {
id: pos_id,
},
},
orderBy: {
attempt_no: 'desc',
select: {
last_tsp_status: true,
id: true,
},
}),
@@ -135,12 +142,25 @@ export class SalesInvoiceTspService {
}),
])
if (!attempt) {
throw new NotFoundException('صورت‌حساب ارسالی فاکتور شما به سامانه یافت نشد.')
if (!invoice) {
throw new NotFoundException('صورت‌حساب ارسالی صورت‌حساب شما به سامانه یافت نشد.')
}
if (!pos) {
throw new NotFoundException('مشکلی در ساختار اطلاعات ورودی شما وجود دارد.')
}
if (
!invoice.last_tsp_status ||
invoice.last_tsp_status === TspProviderResponseStatus.NOT_SEND
) {
throw new BadRequestException(
'صورت‌حساب شما هنوز به سامانه مالیاتی ارسال نشده است. لطفا چند لحظه دیگر مجددا تلاش کنید.',
)
}
if (invoice.last_tsp_status === TspProviderResponseStatus.QUEUED) {
throw new BadRequestException(
'صورت‌حساب شما در صف ارسال به سامانه مالیاتی قرار دارد. لطفا چند لحظه دیگر مجددا تلاش کنید.',
)
}
const { business_activity } = pos.complex
@@ -153,7 +173,7 @@ export class SalesInvoiceTspService {
business_activity.partner_token,
)
return await onResult(this.prisma, result, attempt.id)
return await onResult(this.prisma, result, invoice.id)
}
async correctionSend(
@@ -282,18 +302,18 @@ export class SalesInvoiceTspService {
})
if (!relatedInvoice) {
throw new NotFoundException('فاکتور مورد نظر یافت نشد.')
throw new NotFoundException('صورت‌حساب مورد نظر یافت نشد.')
}
if (relatedInvoice.type === TspProviderRequestType.REVOKE) {
throw new BadRequestException(
'فاکتور ارسالی قبلا ابطال شده است و امکان ویرایش آن وجود ندارد.',
'صورت‌حساب ارسالی قبلا ابطال شده است و امکان ویرایش آن وجود ندارد.',
)
}
if (!relatedInvoice.tax_id) {
throw new BadRequestException(
'فاکتور قبلی همچنان در حال بررسی است و امکان ویرایش آن وجود ندارد.',
'صورت‌حساب قبلی همچنان در حال بررسی است و امکان ویرایش آن وجود ندارد.',
)
}
@@ -226,7 +226,7 @@ export class NamaProviderValidationErrorDto {
}
export class NamaProviderResponseDto {
@ApiProperty({ description: 'شناسه پیگیری فاکتور' })
@ApiProperty({ description: 'شناسه پیگیری صورت‌حساب' })
@IsString()
uuid: string
@@ -21,7 +21,7 @@ export class NamaProviderRevokeHeaderDto {
@ApiProperty({
required: true,
description: 'آخرین شماره مالیاتی دریافت شده مربوط به فاکتور',
description: 'آخرین شماره مالیاتی دریافت شده مربوط به صورت‌حساب',
})
@IsString()
irtaxid: string
@@ -23,44 +23,46 @@ export async function getOriginalResendAttemptNumber(
): Promise<number> {
let attemptNumber = 1
const existingAttempt = await prisma.saleInvoiceTspAttempts.findFirst({
const invoice = await prisma.salesInvoice.findFirst({
where: {
invoice_id,
id: invoice_id,
},
include: {
invoice: {
select: {
type: true,
},
},
select: {
type: true,
last_attempt_no: true,
last_tsp_status: true,
},
orderBy: {
created_at: 'desc',
},
})
if (existingAttempt) {
attemptNumber = existingAttempt.attempt_no + 1
if (existingAttempt.invoice.type !== TspProviderRequestType.ORIGINAL) {
if (invoice) {
const { last_attempt_no, type, last_tsp_status } = invoice
attemptNumber = (last_attempt_no ?? 0) + 1
if (type !== TspProviderRequestType.ORIGINAL) {
throw new BadRequestException(
'فقط فاکتورهای اصلی قابل ارسال مجدد به سامانه مالیاتی هستند.',
'فقط ‌صورت‌حساب‌های اصلی قابل ارسال مجدد به سامانه مالیاتی هستند.',
)
}
if (existingAttempt.status === TspProviderResponseStatus.SUCCESS) {
if (last_tsp_status === TspProviderResponseStatus.SUCCESS) {
throw new BadRequestException(
'فاکتور تایید شده از طرف سازمان مالیاتی قابل ارسال مجدد نیست.',
'صورت‌حساب تایید شده از طرف سازمان مالیاتی قابل ارسال مجدد نیست.',
)
}
if (
existingAttempt.status === TspProviderResponseStatus.QUEUED ||
existingAttempt.status === TspProviderResponseStatus.FISCAL_QUEUED
) {
if (last_tsp_status === TspProviderResponseStatus.QUEUED) {
throw new BadRequestException(
'در حال حاضر فاکتور شما در حال بررسی توسط سازمان مالیاتی است.',
'در حال حاضر صورت‌حساب شما در صف ارسال به سازمان مالیاتی است.',
)
}
}
if (last_tsp_status === TspProviderResponseStatus.FISCAL_QUEUED) {
throw new BadRequestException(
'در حال حاضر صورت‌حساب شما در حال بررسی توسط سازمان مالیاتی است.',
)
}
} else throw new NotFoundException('صورت‌حساب مورد نظر یافت نشد.')
return attemptNumber
}
@@ -138,7 +140,7 @@ export async function buildRevokePayload(
})
if (!invoice) {
throw new NotFoundException('فاکتور مورد نظر یافت نشد.')
throw new NotFoundException('صورت‌حساب مورد نظر یافت نشد.')
}
const { partner } = (invoice.pos.complex.business_activity.consumer.legal ||
@@ -425,7 +427,7 @@ export async function buildOriginalPayload(
})
if (!invoice) {
throw new NotFoundException('فاکتور مورد نظر یافت نشد.')
throw new NotFoundException('صورت‌حساب مورد نظر یافت نشد.')
}
const {
@@ -552,12 +554,12 @@ export async function getRelatedInvoiceForModification(
})
if (!relatedInvoice) {
throw new NotFoundException('فاکتور مورد نظر یافت نشد.')
throw new NotFoundException('صورت‌حساب مورد نظر یافت نشد.')
}
if (relatedInvoice.type === TspProviderRequestType.REVOKE) {
throw new BadRequestException(
'فاکتور ارسالی قبلا ابطال شده است و امکان ویرایش آن وجود ندارد.',
'صورت‌حساب ارسالی قبلا ابطال شده است و امکان ویرایش آن وجود ندارد.',
)
}
@@ -566,7 +568,7 @@ export async function getRelatedInvoiceForModification(
relatedInvoice.tsp_attempts?.[0].status !== TspProviderResponseStatus.SUCCESS
) {
throw new BadRequestException(
'فاکتور قبلی همچنان در حال بررسی است و امکان اصلاح آن وجود ندارد.',
'صورت‌حساب قبلی همچنان در حال بررسی است و امکان اصلاح آن وجود ندارد.',
)
}
@@ -587,11 +589,11 @@ export async function trySend(
export async function onResult(
prisma: PrismaService,
result: TspProviderOriginalResponseDto | TspProviderGetResponseDto,
attempt_id: string,
invoice_id: string,
): Promise<TspProviderActionResponseDto> {
let attemptUpdatedData: SaleInvoiceTspAttemptsUpdateInput = {}
console.log('attempt', result, attempt_id)
console.log('attempt', result)
const resultMessage = result.message
@@ -606,7 +608,7 @@ export async function onResult(
? resultMessage
: result.hasError
? 'وجود مشکل در ارسال به سامانه مالیاتی'
: 'فاکتور با موفقیت به سامانه مالیاتی ارسال شد.',
: 'صورت‌حساب با موفقیت به سامانه مالیاتی ارسال شد.',
invoice: {
update: {
tax_id: result['tax_id'] ? result['tax_id'] : undefined,
@@ -633,26 +635,37 @@ export async function onResult(
attemptUpdatedData = {
status: TspProviderResponseStatus.SEND_FAILURE,
message:
'متاسفانه امکان ارسال فاکتور به سیستم مالیاتی در حال حاضر وجود ندارد. لطفا بعدا تلاش کنید.',
'متاسفانه امکان ارسال صورت‌حساب به سیستم مالیاتی در حال حاضر وجود ندارد. لطفا بعدا تلاش کنید.',
received_at: new Date().toISOString(),
}
}
const updatedAttempt = await prisma.saleInvoiceTspAttempts.update({
where: {
id: attempt_id,
},
data: attemptUpdatedData,
select: {
status: true,
invoice: true,
message: true,
},
const invoice = await prisma.$transaction(async tx => {
const lastAttempt = await tx.saleInvoiceTspAttempts.findFirst({
where: { invoice_id },
orderBy: { attempt_no: 'desc' },
select: { id: true },
})
if (!lastAttempt) throw new NotFoundException('صورت‌حساب مورد نظر یافت نشد.')
await tx.saleInvoiceTspAttempts.update({
where: { id: lastAttempt.id },
data: attemptUpdatedData,
})
const updatedInvoice = await tx.salesInvoice.update({
where: { id: invoice_id },
data: {
last_tsp_status: attemptUpdatedData.status,
},
})
return updatedInvoice
})
return {
invoice: updatedAttempt.invoice,
status: updatedAttempt.status,
message: updatedAttempt.message,
invoice,
status: attemptUpdatedData.status as TspProviderResponseStatus,
message: attemptUpdatedData.message as string,
}
}