feat: enhance sales invoice processing with correction and return functionalities
This commit is contained in:
@@ -83,7 +83,7 @@ export class SharedCreateTerminalPayment {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@ApiProperty({ required: true })
|
||||
terminalId: string
|
||||
terminal_id: string
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
|
||||
@@ -23,6 +23,7 @@ interface TerminalPaymentInfo {
|
||||
interface NormalizedPayment {
|
||||
method: PaymentMethodType
|
||||
amount: number
|
||||
terminalInfo?: TerminalPaymentInfo
|
||||
}
|
||||
|
||||
interface CreateSharedSaleInvoiceInput {
|
||||
@@ -57,10 +58,7 @@ export class SharedSaleInvoiceCreateService {
|
||||
} = input
|
||||
|
||||
const normalizedInvoiceDate = this.normalizeInvoiceDate(data.invoice_date)
|
||||
const { payments, terminalInfo } = this.buildPaymentsData(
|
||||
data.payments,
|
||||
data.total_amount,
|
||||
)
|
||||
const payments = this.buildPaymentsData(data.payments, data.total_amount)
|
||||
|
||||
for (let attempt = 1; attempt <= this.createInvoiceRetries; attempt++) {
|
||||
try {
|
||||
@@ -88,13 +86,7 @@ export class SharedSaleInvoiceCreateService {
|
||||
select: { ...QUERY_CONSTANTS.SALE_INVOICE.select },
|
||||
})
|
||||
|
||||
await this.createPayments(
|
||||
$tx,
|
||||
salesInvoice.id,
|
||||
payments,
|
||||
terminalInfo,
|
||||
normalizedInvoiceDate,
|
||||
)
|
||||
await this.createPayments($tx, salesInvoice.id, payments, normalizedInvoiceDate)
|
||||
|
||||
return salesInvoice
|
||||
})
|
||||
@@ -148,7 +140,7 @@ export class SharedSaleInvoiceCreateService {
|
||||
}
|
||||
|
||||
const rawPayments = (paymentsData || {}) as Record<string, unknown>
|
||||
const terminalInfo = rawPayments.terminals as TerminalPaymentInfo | undefined
|
||||
const terminalPayments = rawPayments.terminals as TerminalPaymentInfo[] | undefined
|
||||
|
||||
const payments: NormalizedPayment[] = Object.entries(rawPayments)
|
||||
.filter(([key, value]) => key !== 'terminals' && value && Number(value) > 0)
|
||||
@@ -158,40 +150,24 @@ export class SharedSaleInvoiceCreateService {
|
||||
}))
|
||||
.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)
|
||||
const hasTerminalPayment = terminalPayments && terminalPayments.length
|
||||
|
||||
if (!hasTerminalPayment && terminalInfo) {
|
||||
const terminalAmount =
|
||||
typeof terminalInfo.amount === 'number'
|
||||
? terminalInfo.amount
|
||||
: Math.max(0, Number(totalAmount) - nonTerminalTotal)
|
||||
|
||||
if (terminalAmount > 0) {
|
||||
if (hasTerminalPayment) {
|
||||
for (const terminal of terminalPayments) {
|
||||
payments.push({
|
||||
method: PaymentMethodType.TERMINAL,
|
||||
amount: terminalAmount,
|
||||
amount: terminal.amount,
|
||||
terminalInfo: terminal,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
this.validatePayments(payments, totalAmount, terminalInfo)
|
||||
this.validatePayments(payments, totalAmount)
|
||||
|
||||
return {
|
||||
payments,
|
||||
terminalInfo,
|
||||
}
|
||||
return payments
|
||||
}
|
||||
|
||||
private validatePayments(
|
||||
payments: NormalizedPayment[],
|
||||
totalAmount: number,
|
||||
terminalInfo?: TerminalPaymentInfo,
|
||||
) {
|
||||
private validatePayments(payments: NormalizedPayment[], totalAmount: number) {
|
||||
const totalPayments = payments.reduce((sum, payment) => sum + payment.amount, 0)
|
||||
const roundedTotalPayments = Number(totalPayments.toFixed(2))
|
||||
const roundedTotalAmount = Number(Number(totalAmount).toFixed(2))
|
||||
@@ -199,14 +175,6 @@ export class SharedSaleInvoiceCreateService {
|
||||
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(
|
||||
@@ -541,7 +509,6 @@ export class SharedSaleInvoiceCreateService {
|
||||
tx: Prisma.TransactionClient,
|
||||
invoiceId: string,
|
||||
payments: NormalizedPayment[],
|
||||
terminalInfo: TerminalPaymentInfo | undefined,
|
||||
paidAt: Date,
|
||||
) {
|
||||
for (const payment of payments) {
|
||||
@@ -557,16 +524,18 @@ export class SharedSaleInvoiceCreateService {
|
||||
},
|
||||
})
|
||||
|
||||
if (payment.method === PaymentMethodType.TERMINAL && terminalInfo) {
|
||||
if (payment.method === PaymentMethodType.TERMINAL && payment.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,
|
||||
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,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { IPosPayload } from '@/common/models'
|
||||
import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common'
|
||||
import { ApiTags } from '@nestjs/swagger'
|
||||
import { ConsumerSaleInvoicesFilterDto } from './dto/saleInvoices-filter.dto'
|
||||
import { PosCorrectionSalesInvoiceDto } from '../../pos/sales-invoices/dto/create-sales-invoice.dto'
|
||||
import type {
|
||||
SaleInvoicesServiceFindAllResponseDto,
|
||||
SaleInvoicesServiceFindOneResponseDto,
|
||||
@@ -59,4 +60,18 @@ export class StatisticsController {
|
||||
revoke(@Param('id') id: string, @PosInfo() posInfo: IPosPayload) {
|
||||
return this.service.revoke(id, posInfo)
|
||||
}
|
||||
|
||||
@Post(':id/correction')
|
||||
correction(
|
||||
@Param('id') id: string,
|
||||
@PosInfo() posInfo: IPosPayload,
|
||||
@Body() data: PosCorrectionSalesInvoiceDto,
|
||||
) {
|
||||
return this.service.correction(id, posInfo, data)
|
||||
}
|
||||
|
||||
@Post(':id/return')
|
||||
return(@Param('id') id: string, @PosInfo() posInfo: IPosPayload) {
|
||||
return this.service.return(id, posInfo)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import { PrismaService } from '@/prisma/prisma.service'
|
||||
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'
|
||||
import { ResponseMapper } from 'common/response/response-mapper'
|
||||
import { PosCorrectionSalesInvoiceDto } from '../../pos/sales-invoices/dto/create-sales-invoice.dto'
|
||||
import { SalesInvoiceTspService } from '../../tspProviders/sales-invoice-tsp.service'
|
||||
import { ConsumerSaleInvoicesFilterDto } from './dto/saleInvoices-filter.dto'
|
||||
|
||||
@@ -177,6 +178,39 @@ export class SaleInvoicesService {
|
||||
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) {
|
||||
const invoice = await this.sharedSaleInvoiceActionsService.inquiry(
|
||||
consumer_account_id,
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
getOriginalResendAttemptNumber,
|
||||
getRelatedInvoiceForCorrection,
|
||||
onResult,
|
||||
trySend,
|
||||
} from './utils/sales-invoice-tsp.utils'
|
||||
|
||||
type ItemTspRow = {
|
||||
@@ -228,43 +227,9 @@ export class SalesInvoiceTspService {
|
||||
},
|
||||
)
|
||||
|
||||
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()
|
||||
}
|
||||
const result = await this.tspSwitchService.correction(correctionPayload)
|
||||
|
||||
return onResult(this.prisma, result, attempt.id)
|
||||
|
||||
// const countByGoodId = (goodIds: string[]): Map<string, number> => {
|
||||
// const counts = new Map<string, number>()
|
||||
// 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
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
return await onResult(this.prisma, result, newInvoice.id)
|
||||
}
|
||||
|
||||
async revoke(
|
||||
@@ -317,71 +282,72 @@ export class SalesInvoiceTspService {
|
||||
)
|
||||
}
|
||||
|
||||
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 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,
|
||||
// terminal_id: 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,
|
||||
// @ts-ignore
|
||||
payments: undefined,
|
||||
settlement_type: relatedInvoice.settlement_type,
|
||||
items: relatedInvoice.items.map(item => ({
|
||||
unit_price: Number(item.unit_price),
|
||||
|
||||
@@ -68,7 +68,7 @@ export class NamaProviderUtils {
|
||||
sstid: item.sku,
|
||||
vra: item.sku_vat,
|
||||
fee: String(item.unit_price),
|
||||
dis: String(item.discount_amount),
|
||||
dis: String(parseInt(item.discount_amount + '')),
|
||||
mu: item.measure_unit,
|
||||
am: String(item.quantity),
|
||||
consfee: '0',
|
||||
@@ -120,7 +120,7 @@ export class NamaProviderUtils {
|
||||
sstid: item.sku,
|
||||
vra: item.sku_vat,
|
||||
fee: String(item.unit_price),
|
||||
dis: String(item.discount_amount),
|
||||
dis: String(parseInt(item.discount_amount + '')),
|
||||
mu: item.measure_unit,
|
||||
am: String(item.quantity),
|
||||
consfee: '0',
|
||||
@@ -297,6 +297,9 @@ export class NamaProviderUtils {
|
||||
}
|
||||
|
||||
private mapPayments(payments: PaymentInfoDto[]): NamaProviderPaymentInfoDto[] {
|
||||
if (!payments) {
|
||||
return []
|
||||
}
|
||||
return payments.map(payment => ({
|
||||
pmt: this.mapPaymentMethod(payment.payment_method),
|
||||
pv: payment.amount,
|
||||
|
||||
@@ -653,6 +653,8 @@ export async function onResult(
|
||||
data: attemptUpdatedData,
|
||||
})
|
||||
|
||||
console.log('attemptUpdatedData', attemptUpdatedData)
|
||||
|
||||
const updatedInvoice = await tx.salesInvoice.update({
|
||||
where: { id: invoice_id },
|
||||
data: {
|
||||
|
||||
Reference in New Issue
Block a user