import { QUERY_CONSTANTS } from '@/common/queryConstants' import { SalesInvoiceCreateInput } from '@/generated/prisma/models' import { PrismaService } from '@/prisma/prisma.service' import { BadRequestException, Injectable } from '@nestjs/common' import { Prisma } from 'generated/prisma/client' import { CustomerType, PaymentMethodType, TspProviderRequestType, } from 'generated/prisma/enums' import { SharedCreateSalesInvoiceDto } from './sale-invoice-create.dto' interface TerminalPaymentInfo { terminal_id: string stan: string rrn: string transaction_date_time: string | Date customer_card_no: string description?: string amount: number } interface NormalizedPayment { method: PaymentMethodType amount: number } interface CreateSharedSaleInvoiceInput { tx?: Prisma.TransactionClient data: SharedCreateSalesInvoiceDto businessId: string complexId: string posId: string consumerAccountId: string type: TspProviderRequestType main_invoice_id?: string ref_invoice_id?: string } @Injectable() export class SharedSaleInvoiceCreateService { private readonly createInvoiceRetries = 3 constructor(private prisma: PrismaService) {} async create(input: CreateSharedSaleInvoiceInput) { const { tx = this.prisma, data, businessId, complexId, posId, consumerAccountId, type, main_invoice_id, ref_invoice_id, } = input const normalizedInvoiceDate = this.normalizeInvoiceDate(data.invoice_date) const { payments, terminalInfo } = this.buildPaymentsData( data.payments, data.total_amount, ) for (let attempt = 1; attempt <= this.createInvoiceRetries; attempt++) { try { return await tx.$transaction(async $tx => { const invoiceNumber = await this.getNextInvoiceNumber($tx, businessId) const customerId = await this.resolveCustomerId($tx, data, businessId) const goodsById = await this.getGoodsById($tx, data) const salesInvoiceData = this.buildSalesInvoiceData({ data, normalizedInvoiceDate, invoiceNumber, consumerAccountId, businessId, complexId, posId, goodsById, customerId, type, main_invoice_id, ref_invoice_id, }) const salesInvoice = await $tx.salesInvoice.create({ data: salesInvoiceData, select: { ...QUERY_CONSTANTS.SALE_INVOICE.select }, }) await this.createPayments( $tx, salesInvoice.id, payments, terminalInfo, normalizedInvoiceDate, ) return salesInvoice }) } catch (error) { if ( this.isRetryableInvoiceConflict(error) && attempt < this.createInvoiceRetries ) { continue } throw error } } throw new BadRequestException('ایجاد فاکتور با خطا مواجه شد.') } private isRetryableInvoiceConflict(error: unknown) { if (!(error instanceof Prisma.PrismaClientKnownRequestError)) { return false } if (error.code !== 'P2002') { return false } const target = (error.meta?.target as string[]) || [] return ( target.includes('invoice_number') || target.includes('sales_invoices_invoice_number_pos_id_key') ) } private normalizeInvoiceDate(invoiceDate: Date | string) { return new Date(invoiceDate).toISOString() as any } private buildPaymentsData( paymentsData: SharedCreateSalesInvoiceDto['payments'], totalAmount: number, ) { const paymentMethodMap: Record = { cash: PaymentMethodType.CASH, set_off: PaymentMethodType.SET_OFF, card: PaymentMethodType.CARD, payment_gateway: PaymentMethodType.PAYMENT_GATEWAY, bank: PaymentMethodType.BANK, check: PaymentMethodType.CHEQUE, other: PaymentMethodType.OTHER, terminal: PaymentMethodType.TERMINAL, } const rawPayments = (paymentsData || {}) as Record const terminalInfo = rawPayments.terminals as TerminalPaymentInfo | undefined const payments: NormalizedPayment[] = Object.entries(rawPayments) .filter(([key, value]) => key !== 'terminals' && value && Number(value) > 0) .map(([key, value]) => ({ method: paymentMethodMap[key.toLowerCase()], amount: typeof value === 'number' ? value : Number(value || 0), })) .filter(payment => payment.method && payment.amount > 0) as NormalizedPayment[] const hasTerminalPayment = payments.some( payment => payment.method === PaymentMethodType.TERMINAL, ) const nonTerminalTotal = payments .filter(payment => payment.method !== PaymentMethodType.TERMINAL) .reduce((sum, payment) => sum + payment.amount, 0) if (!hasTerminalPayment && terminalInfo) { const terminalAmount = typeof terminalInfo.amount === 'number' ? terminalInfo.amount : Math.max(0, Number(totalAmount) - nonTerminalTotal) if (terminalAmount > 0) { payments.push({ method: PaymentMethodType.TERMINAL, amount: terminalAmount, }) } } this.validatePayments(payments, totalAmount, terminalInfo) return { payments, terminalInfo, } } private validatePayments( payments: NormalizedPayment[], totalAmount: number, terminalInfo?: TerminalPaymentInfo, ) { const totalPayments = payments.reduce((sum, payment) => sum + payment.amount, 0) const roundedTotalPayments = Number(totalPayments.toFixed(2)) const roundedTotalAmount = Number(Number(totalAmount).toFixed(2)) if (roundedTotalPayments !== roundedTotalAmount) { throw new BadRequestException('مبلغ پرداختی باید برابر با مبلغ کل فاکتور باشد.') } const terminalPayments = payments.filter( payment => payment.method === PaymentMethodType.TERMINAL, ) if (terminalPayments.length > 0 && !terminalInfo) { throw new BadRequestException('برای پرداخت ترمینال اطلاعات ترمینال الزامی است.') } } private async resolveCustomerId( tx: Prisma.TransactionClient, data: SharedCreateSalesInvoiceDto, businessId: string, ) { if (data.customer_id) { return data.customer_id } if ( data.customer_type === CustomerType.INDIVIDUAL && data.customer?.customer_individual ) { const { national_id, mobile_number, ...rest } = data.customer.customer_individual const customerIndividual = await tx.customerIndividual.upsert({ where: { business_activity_id_national_id: { business_activity_id: businessId, national_id: data.customer.customer_individual.national_id, }, }, create: { ...data.customer.customer_individual, customer: { create: { type: CustomerType.INDIVIDUAL, }, }, business_activity: { connect: { id: businessId, }, }, }, update: { ...rest }, select: { customer_id: true, }, }) if (!customerIndividual) { throw new BadRequestException('متاسفانه مشکلی در اطلاعات مشتری وجود دارد.') } return customerIndividual.customer_id } if (data.customer_type === CustomerType.LEGAL && data.customer?.customer_legal) { const { registration_number, ...rest } = data.customer.customer_legal const customerLegal = await tx.customerLegal.upsert({ where: { business_activity_id_economic_code: { business_activity_id: businessId, economic_code: data.customer.customer_legal.economic_code, }, }, create: { ...data.customer.customer_legal, customer: { create: { type: CustomerType.LEGAL, }, }, business_activity: { connect: { id: businessId, }, }, }, update: { ...rest, }, select: { customer_id: true, }, }) if (!customerLegal) { throw new BadRequestException('متاسفانه مشکلی در اطلاعات مشتری وجود دارد.') } return customerLegal.customer_id } return null } private async getGoodsById( tx: Prisma.TransactionClient, data: SharedCreateSalesInvoiceDto, ) { const itemGoodIds = data.items .map(item => item.good_id) .filter((goodId): goodId is string => Boolean(goodId)) const goods = itemGoodIds.length ? await tx.good.findMany({ where: { id: { in: itemGoodIds, }, }, select: { id: true, name: true, sku: { select: { id: true, name: true, code: true, VAT: true, }, }, local_sku: true, barcode: true, pricing_model: true, measure_unit: { select: { id: true, name: true, code: true, }, }, base_sale_price: true, image_url: true, category: { select: { id: true, name: true, }, }, }, }) : [] return new Map(goods.map(good => [good.id, good])) } private buildSalesInvoiceData(params: { data: SharedCreateSalesInvoiceDto normalizedInvoiceDate: Date invoiceNumber: number consumerAccountId: string businessId: string complexId: string posId: string goodsById: Map customerId: string | null type: TspProviderRequestType main_invoice_id?: string ref_invoice_id?: string }) { const { data, normalizedInvoiceDate, invoiceNumber, consumerAccountId, posId, goodsById, customerId, businessId, complexId, type, main_invoice_id, ref_invoice_id, } = params const { customer_id, customer_type, customer, payments, settlement_type, ...invoiceData } = data if ( type !== TspProviderRequestType.ORIGINAL && !(main_invoice_id || ref_invoice_id) ) { throw new BadRequestException('متاسفانه مشکلی در اطلاعات فاکتور وجود دارد.') } const salesInvoiceData: SalesInvoiceCreateInput = { ...invoiceData, invoice_date: normalizedInvoiceDate, invoice_number: invoiceNumber, total_amount: data.total_amount, discount_amount: data.items.reduce( (prev, curr) => (prev += curr.discount_amount), 0, ), tax_amount: data.items.reduce((prev, curr) => (prev += curr.tax_amount), 0), code: this.generateInvoiceCode(businessId, complexId, posId, invoiceNumber), type, settlement_type, items: { createMany: { data: data.items.map(item => ({ good_id: item.good_id!, quantity: item.quantity, unit_price: item.unit_price, total_amount: item.total_amount, discount_amount: item.discount_amount, tax_amount: item.tax_amount, measure_unit_text: goodsById.get(item.good_id!)?.measure_unit.name || null, measure_unit_code: goodsById.get(item.good_id!)?.measure_unit.code || null, sku_code: goodsById.get(item.good_id!)?.sku.code || null, sku_vat: goodsById.get(item.good_id!)?.sku.VAT || null, payload: item.payload ? JSON.parse(JSON.stringify(item.payload)) : undefined, good_snapshot: item.good_id ? JSON.parse(JSON.stringify(goodsById.get(item.good_id) || null)) : undefined, })), }, }, unknown_customer: {}, consumer_account: { connect: { id: consumerAccountId, }, }, pos: { connect: { id: posId, }, }, } if (customerId) { salesInvoiceData.customer = { connect: { id: customerId, }, } } if (type !== TspProviderRequestType.ORIGINAL) { salesInvoiceData.main_id = main_invoice_id salesInvoiceData.reference_invoice = { connect: { id: ref_invoice_id, }, } } return salesInvoiceData } private async getNextInvoiceNumber(tx: Prisma.TransactionClient, businessId: string) { const latestInvoice = await tx.salesInvoice.findFirst({ where: { pos: { complex: { business_activity_id: businessId, }, }, }, orderBy: { invoice_number: 'desc', }, select: { invoice_number: true, pos: { select: { complex: { select: { business_activity: { select: { invoice_number_sequence: true, }, }, }, }, }, }, }, }) const latestSequence = latestInvoice?.pos?.complex?.business_activity?.invoice_number_sequence.toNumber() || 0 return Math.max(latestInvoice?.invoice_number || 0, latestSequence) + 1 } private async createPayments( tx: Prisma.TransactionClient, invoiceId: string, payments: NormalizedPayment[], terminalInfo: TerminalPaymentInfo | undefined, paidAt: Date, ) { for (const payment of payments) { if (payment.amount <= 0) { continue } const createdPayment = await tx.salesInvoicePayment.create({ data: { invoice_id: invoiceId, amount: payment.amount, payment_method: payment.method, paid_at: paidAt, }, }) if (payment.method === PaymentMethodType.TERMINAL && terminalInfo) { await tx.salesInvoicePaymentTerminalInfo.create({ data: { payment_id: createdPayment.id, terminal_id: terminalInfo.terminal_id, stan: terminalInfo.stan, rrn: terminalInfo.rrn, transaction_date_time: new Date(terminalInfo.transaction_date_time || ''), customer_card_no: terminalInfo.customer_card_no || null, description: terminalInfo.description || null, }, }) } } } private generateInvoiceCode( businessId: string, complexId: string, posId: string, invoiceNumber: number, ) { return `${businessId.substring(businessId.length - 2, businessId.length)}${complexId.substring(complexId.length - 2, complexId.length)}${posId.substring(posId.length - 2, posId.length)}${invoiceNumber.toString()}` } }