import { SharedSaleInvoiceCreateService } from '@/common/services/saleInvoices/sale-invoice-create.service' import { PrismaService } from '@/prisma/prisma.service' import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common' import { CustomerType, TspProviderRequestType, TspProviderResponseStatus, } from 'generated/prisma/client' import { TspProviderActionResponseDto, TspProviderCorrectionInvoicePayloadDto, } from './dto' import { SalesInvoiceTspSwitchService } from './sales-invoice-tsp-switch.service' import { buildCorrectionPayload, buildOriginalPayload, buildRevokePayload, getOriginalResendAttemptNumber, getRelatedInvoiceForCorrection, onResult, trySend, } from './utils/sales-invoice-tsp.utils' type ItemTspRow = { tsp_provider: string | null tax_id: string | null } @Injectable() export class SalesInvoiceTspService { constructor( private readonly prisma: PrismaService, private readonly tspSwitchService: SalesInvoiceTspSwitchService, private sharedSaleInvoiceCreateService: SharedSaleInvoiceCreateService, ) {} async originalSend( posId: string, invoice_id: string, ): Promise { const payload = await buildOriginalPayload(this.prisma, invoice_id, posId) const attemptNumber = await getOriginalResendAttemptNumber(this.prisma, invoice_id) 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.send(payload) return await onResult(this.prisma, result, invoice.id) } async sendBulk(consumer_id: string, invoice_ids: string[]): Promise { // if (!invoice_ids.length) return // const payloads: TspProviderOriginalSendPayloadDto[] = [] // for (const invoiceId of invoice_ids) { // const posId = await this.getPosId(consumer_id, invoiceId) // payloads.push(await this.buildPayload(invoiceId, posId)) // } // const bulkResult = await this.tspSwitchService.sendBulk(payloads) // const allItemResults = bulkResult.flatMap(result => result.items) // await this.persistAttemptResults(allItemResults) } async get( invoice_id: string, pos_id: string, consumer_id: string, ): Promise { const [invoice, pos] = await this.prisma.$transaction(async tx => [ await tx.salesInvoice.findUnique({ where: { id: invoice_id, pos: { id: pos_id, }, }, select: { last_tsp_status: true, id: true, }, }), await tx.pos.findUnique({ where: { id: pos_id, complex: { business_activity: { consumer_id, }, }, }, select: { complex: { select: { business_activity: { select: { partner_token: true, consumer: { select: { legal: { select: { partner: { select: { tsp_provider: true, }, }, }, }, individual: { select: { partner: { select: { tsp_provider: true, }, }, }, }, }, }, }, }, }, }, }, }), ]) 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 const { partner } = (business_activity.consumer.individual || business_activity.consumer.legal)! const result = await this.tspSwitchService.get( partner.tsp_provider, invoice_id, business_activity.partner_token, ) return await onResult(this.prisma, result, invoice.id) } async correctionSend( consumerAccountId: string, posId: string, complexId: string, businessId: string, ref_invoice_id: string, dataToUpdate: TspProviderCorrectionInvoicePayloadDto, ): Promise { const [newInvoice, attempt, correctionPayload] = await this.prisma.$transaction( async tx => { const relatedInvoice = await getRelatedInvoiceForCorrection(tx, ref_invoice_id) 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 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 result = await this.runProviderCall(() => trySend(this.tspSwitchService, correctionPayload), ) if (result?.hasError) { result.provider_request_payload = result.provider_request_payload || JSON.parse(JSON.stringify(correctionPayload)) result.sent_at = result.sent_at || new Date().toISOString() result.received_at = result.received_at || new Date().toISOString() } return onResult(this.prisma, result, attempt.id) // const countByGoodId = (goodIds: string[]): Map => { // const counts = new Map() // 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 // if (updatedCounts.size !== previousCounts.size) { // isBackFromSale = true // } else { // for (const [goodId, count] of updatedCounts) { // if (previousCounts.get(goodId) !== count) { // isBackFromSale = true // break // } // } // } } async revoke( consumerAccountId: string, posId: string, complexId: string, businessId: string, ref_invoice_id: string, ): Promise { const [newInvoice, attempt, revokePayload] = await this.prisma.$transaction( async tx => { const relatedInvoice = await tx.salesInvoice.findUnique({ where: { id: ref_invoice_id, }, include: { customer: { select: { type: true, }, }, tsp_attempts: { orderBy: { attempt_no: 'desc', }, take: 1, }, payments: { include: { terminal_info: true, }, }, items: true, }, }) if (!relatedInvoice) { throw new NotFoundException('صورت‌حساب مورد نظر یافت نشد.') } if (relatedInvoice.type === TspProviderRequestType.REVOKE) { throw new BadRequestException( 'صورت‌حساب ارسالی قبلا ابطال شده است و امکان ویرایش آن وجود ندارد.', ) } if (!relatedInvoice.tax_id) { throw new BadRequestException( 'صورت‌حساب قبلی همچنان در حال بررسی است و امکان ویرایش آن وجود ندارد.', ) } const payments = relatedInvoice.payments.reduce( (acc, payment) => { const amount = Number(payment.amount) switch (payment.payment_method) { case 'CASH': acc.cash = (acc.cash || 0) + amount break case 'SET_OFF': acc.set_off = (acc.set_off || 0) + amount break case 'CARD': acc.card = (acc.card || 0) + amount break case 'BANK': acc.bank = (acc.bank || 0) + amount break case 'CHEQUE': acc.check = (acc.check || 0) + amount break case 'OTHER': acc.other = (acc.other || 0) + amount break case 'TERMINAL': acc.terminals = payment.terminal_info ? { amount, terminalId: payment.terminal_info.terminal_id, stan: payment.terminal_info.stan, rrn: payment.terminal_info.rrn, customer_card_no: payment.terminal_info.customer_card_no || undefined, transaction_date_time: payment.terminal_info.transaction_date_time, description: payment.terminal_info.description || undefined, } : acc.terminals break default: break } return acc }, {} as { terminals?: { amount?: number terminalId: string stan: string rrn: string customer_card_no?: string transaction_date_time: Date description?: string } cash?: number set_off?: number card?: number bank?: number check?: number other?: number }, ) const newInvoice = await this.sharedSaleInvoiceCreateService.create({ data: { customer_type: relatedInvoice.customer?.type || CustomerType.UNKNOWN, invoice_date: new Date(), payments, settlement_type: relatedInvoice.settlement_type, items: relatedInvoice.items.map(item => ({ unit_price: Number(item.unit_price), quantity: Number(item.quantity), total_amount: Number(item.total_amount), discount_amount: Number(item.discount_amount || 0), tax_amount: Number(item.tax_amount || 0), invoice_id: '', good_id: item.good_id, service_id: item.service_id || undefined, payload: item.payload as any, notes: item.notes || '', })), total_amount: Number(relatedInvoice.total_amount), discount_amount: Number(relatedInvoice.discount_amount), tax_amount: Number(relatedInvoice.tax_amount), customer_id: relatedInvoice.customer_id || undefined, }, businessId, complexId, posId, consumerAccountId, main_invoice_id: relatedInvoice.main_id || relatedInvoice.id, ref_invoice_id: relatedInvoice.id, type: TspProviderRequestType.REVOKE, }) const payload = await buildRevokePayload(this.prisma, newInvoice.id, posId) const attempt = await tx.saleInvoiceTspAttempts.create({ data: { attempt_no: 1, invoice_id: newInvoice.id, message: 'در حال ارسال به سامانه مالیاتی...', status: TspProviderResponseStatus.QUEUED, raw_request_payload: JSON.parse(JSON.stringify(payload)), provider_request_payload: {}, }, }) return [newInvoice, attempt, payload] }, ) const result = await this.runProviderCall(() => this.tspSwitchService.revoke(revokePayload), ) if (result?.hasError) { result.provider_request_payload = result.provider_request_payload || JSON.parse(JSON.stringify(revokePayload)) result.sent_at = result.sent_at || new Date().toISOString() result.received_at = result.received_at || new Date().toISOString() } return await onResult(this.prisma, result, attempt.id) } private mapProviderError(error: any) { return { hasError: true, status: error.status || TspProviderResponseStatus.FAILURE, message: error.message?.toString() || 'خطا در ارتباط با سامانه مالیاتی. لطفا مجددا تلاش کنید.', provider_response_payload: error.provider_response_payload || { error: error?.message || 'fetch failed', cause: error?.cause || null, }, sent_at: error.sent_at || new Date().toISOString(), received_at: error.received_at || new Date().toISOString(), } } private async runProviderCall(fn: () => Promise) { try { return await fn() } catch (error: any) { return this.mapProviderError(error) } } }