feat: add stock keeping unit management with create, update, and retrieval functionalities

- Implemented CreateStockKeepingUnitDto for creating stock keeping units.
- Added StockKeepingUnitsService for handling business logic related to stock keeping units.
- Created StockKeepingUnitsController to manage HTTP requests for stock keeping units.
- Developed UpdateStockKeepingUnitDto for updating existing stock keeping units.
- Introduced StockKeepingUnitsServiceFindAllResponseDto for response structure.
- Established stock keeping units module for encapsulation of related components.

feat: enhance sales invoice fiscal management with new endpoints

- Created SalesInvoicesFilterDto for filtering sales invoices.
- Implemented PosSalesInvoiceFiscalController for managing fiscal operations on sales invoices.
- Developed PosSalesInvoiceFiscalService to handle fiscal logic and interactions.
- Added methods for sending, retrying, and checking the status of fiscal invoices.
- Integrated error handling and response mapping for fiscal operations.
This commit is contained in:
2026-05-01 19:43:59 +03:30
parent a68a7f594d
commit ad470d2166
91 changed files with 8973 additions and 2469 deletions
@@ -4,7 +4,7 @@ import { IsNumber, IsString, MaxLength, MinLength } from 'class-validator'
export class CreateCustomerLegalDto {
@IsString()
@ApiProperty({ required: true })
company_name: string
name: string
@IsString()
@ApiProperty({ required: true })
+6 -2
View File
@@ -13,8 +13,8 @@ export class CreateGoodDto {
description?: string
@IsString()
@ApiProperty()
sku: string
@ApiProperty({ required: true })
sku_id: string
@IsString()
@ApiProperty()
@@ -42,6 +42,10 @@ export class CreateGoodDto {
@IsNumber()
@ApiProperty({ required: false })
base_sale_price?: number
@IsString()
@ApiProperty({ required: true })
measure_unit_id: string
}
export class UpdateGoodDto extends PartialType(CreateGoodDto) {}
+23 -3
View File
@@ -14,11 +14,21 @@ export class GoodsService {
barcode: true,
created_at: true,
description: true,
sku: true,
sku: {
select: {
id: true,
name: true,
},
},
local_sku: true,
is_default_guild_good: true,
pricing_model: true,
unit_type: true,
measure_unit: {
select: {
id: true,
name: true,
},
},
image_url: true,
category: {
select: {
@@ -66,7 +76,7 @@ export class GoodsService {
}
async create(data: CreateGoodDto, business_activity_id: string) {
const { category_id, ...rest } = data
const { category_id, sku_id, measure_unit_id, ...rest } = data
const dataToCreate = {
...rest,
@@ -81,6 +91,16 @@ export class GoodsService {
const good = await this.prisma.good.create({
data: {
...rest,
measure_unit: {
connect: {
id: measure_unit_id,
},
},
sku: {
connect: {
id: sku_id,
},
},
category: {
connect: {
id: category_id,
@@ -0,0 +1,95 @@
import { ApiPropertyOptional } from '@nestjs/swagger'
import { Type } from 'class-transformer'
import {
IsDateString,
IsEnum,
IsNumber,
IsOptional,
IsString,
Min,
} from 'class-validator'
import { FiscalResponseStatus } from 'generated/prisma/enums'
export class SalesInvoicesFilterDto {
@ApiPropertyOptional({ default: 1 })
@IsOptional()
@Type(() => Number)
@IsNumber()
@Min(1)
page?: number
@ApiPropertyOptional({ default: 10 })
@IsOptional()
@Type(() => Number)
@IsNumber()
@Min(1)
perPage?: number
@ApiPropertyOptional()
@IsOptional()
@IsDateString()
invoice_date_from?: string
@ApiPropertyOptional()
@IsOptional()
@IsDateString()
invoice_date_to?: string
@ApiPropertyOptional()
@IsOptional()
@IsDateString()
created_at_from?: string
@ApiPropertyOptional()
@IsOptional()
@IsDateString()
created_at_to?: string
@ApiPropertyOptional()
@IsOptional()
@IsString()
code?: string
@ApiPropertyOptional()
@IsOptional()
@IsString()
customer_name?: string
@ApiPropertyOptional()
@IsOptional()
@IsString()
customer_mobile?: string
@ApiPropertyOptional()
@IsOptional()
@IsString()
customer_national_id?: string
@ApiPropertyOptional()
@IsOptional()
@IsString()
customer_economic_code?: string
@ApiPropertyOptional({ enum: FiscalResponseStatus })
@IsOptional()
@IsEnum(FiscalResponseStatus)
status?: FiscalResponseStatus
@ApiPropertyOptional()
@IsOptional()
@Type(() => Number)
@IsNumber()
total_amount?: number
@ApiPropertyOptional()
@IsOptional()
@Type(() => Number)
@IsNumber()
total_amount_from?: number
@ApiPropertyOptional()
@IsOptional()
@Type(() => Number)
@IsNumber()
total_amount_to?: number
}
@@ -0,0 +1,43 @@
import { PosInfo } from '@/common/decorators/posInfo.decorator'
import type { IPosPayload } from '@/common/models/posPayload.model'
import { Controller, Get, Param, Post } from '@nestjs/common'
import { PosSalesInvoiceFiscalService } from './sales-invoice-fiscal.service'
@Controller('pos/sales_invoices/:invoiceId/fiscal')
export class PosSalesInvoiceFiscalController {
constructor(private readonly service: PosSalesInvoiceFiscalService) {}
@Post('send')
async send(@PosInfo() posInfo: IPosPayload, @Param('invoiceId') invoiceId: string) {
return this.service.send(invoiceId, posInfo)
}
@Post('retry')
async retry(@PosInfo() posInfo: IPosPayload, @Param('invoiceId') invoiceId: string) {
return this.service.retry(invoiceId, posInfo)
}
@Get('status')
async getStatus(
@PosInfo() posInfo: IPosPayload,
@Param('invoiceId') invoiceId: string,
) {
return this.service.getStatus(invoiceId, posInfo)
}
@Post('status/refresh')
async refreshStatus(
@PosInfo() posInfo: IPosPayload,
@Param('invoiceId') invoiceId: string,
) {
return this.service.refreshStatus(invoiceId, posInfo)
}
@Get('attempts')
async getAttempts(
@PosInfo() posInfo: IPosPayload,
@Param('invoiceId') invoiceId: string,
) {
return this.service.getAttempts(invoiceId, posInfo)
}
}
@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common'
import { SalesInvoiceFiscalSwitchService } from '../../../consumer/saleInvoices/fiscal/sales-invoice-fiscal-switch.service'
import { NamaTaxSwitchAdapter } from '../../../consumer/saleInvoices/fiscal/switch/nama/nama-fiscal-switch.adapter'
import { PosSalesInvoiceFiscalController } from './sales-invoice-fiscal.controller'
import { PosSalesInvoiceFiscalService } from './sales-invoice-fiscal.service'
@Module({
controllers: [PosSalesInvoiceFiscalController],
providers: [PosSalesInvoiceFiscalService, SalesInvoiceFiscalSwitchService, NamaTaxSwitchAdapter],
exports: [PosSalesInvoiceFiscalService],
})
export class PosSalesInvoiceFiscalModule {}
@@ -0,0 +1,411 @@
import { IPosPayload } from '@/common/models/posPayload.model'
import { PrismaService } from '@/prisma/prisma.service'
import { Injectable, NotFoundException } from '@nestjs/common'
import {
FiscalInvoiceCustomerType,
FiscalRequestType,
FiscalResponseStatus,
} from 'generated/prisma/enums'
import {
TaxSendStatus,
TaxSwitchGetResultDto,
TaxSwitchSendItemResultDto,
TaxSwitchSendPayloadDto,
} from '../../../consumer/saleInvoices/fiscal/dto/tax-switch.dto'
import { SalesInvoiceFiscalSwitchService } from '../../../consumer/saleInvoices/fiscal/sales-invoice-fiscal-switch.service'
@Injectable()
export class PosSalesInvoiceFiscalService {
constructor(
private readonly prisma: PrismaService,
private readonly fiscalSwitchService: SalesInvoiceFiscalSwitchService,
) {}
async send(invoiceId: string, posInfo: IPosPayload) {
const payload = await this.buildPayload(invoiceId, posInfo)
const itemResults = await this.safeSend(payload)
const attempt = await this.persistAttempt(invoiceId, itemResults)
return {
invoice_id: invoiceId,
fiscal_id: attempt.fiscal_id,
status: attempt.status,
attempt_no: attempt.attempt_no,
}
}
async retry(invoiceId: string, posInfo: IPosPayload) {
return this.send(invoiceId, posInfo)
}
async getStatus(invoiceId: string, posInfo: IPosPayload) {
const invoice = await this.getInvoiceForBusiness(invoiceId, posInfo.business_id)
if (!invoice.fiscal) {
return {
invoice_id: invoiceId,
status: FiscalResponseStatus.NOT_SEND,
fiscal: null,
}
}
const latestAttempt = invoice.fiscal.attempts[0] || null
return {
invoice_id: invoiceId,
status: latestAttempt?.status || FiscalResponseStatus.NOT_SEND,
fiscal: {
id: invoice.fiscal.id,
retry_count: invoice.fiscal.retry_count,
last_attempt_at: invoice.fiscal.last_attempt_at,
},
latest_attempt: latestAttempt,
}
}
async refreshStatus(invoiceId: string, posInfo: IPosPayload) {
const invoice = await this.getInvoiceForBusiness(invoiceId, posInfo.business_id)
const latestAttempt = invoice.fiscal?.attempts?.[0]
if (!invoice.fiscal || !latestAttempt?.tax_id) {
return this.getStatus(invoiceId, posInfo)
}
const providerCode = invoice.pos.provider?.code
const switchResult = await this.fiscalSwitchService.get(
providerCode,
latestAttempt.tax_id,
)
const mappedStatus = this.mapSwitchGetStatus(switchResult.status)
const refreshedAttempt = await this.prisma.$transaction(async tx => {
const nextNo = (latestAttempt.attempt_no || 0) + 1
const createdAttempt = await tx.saleInvoiceFiscalAttempts.create({
data: {
fiscal_id: invoice.fiscal!.id,
attempt_no: nextNo,
status: mappedStatus,
tax_id: switchResult.tax_id,
response_payload: switchResult.response_payload
? JSON.parse(JSON.stringify(switchResult.response_payload))
: undefined,
received_at: switchResult.received_at
? new Date(switchResult.received_at)
: null,
type: FiscalRequestType.MAIN,
},
})
await tx.saleInvoiceFiscals.update({
where: { id: invoice.fiscal!.id },
data: {
retry_count: nextNo,
last_attempt_at: createdAttempt.created_at,
},
})
return createdAttempt
})
return {
invoice_id: invoiceId,
status: refreshedAttempt.status,
latest_attempt: refreshedAttempt,
}
}
async getAttempts(invoiceId: string, posInfo: IPosPayload) {
const invoice = await this.getInvoiceForBusiness(invoiceId, posInfo.business_id)
if (!invoice.fiscal) {
return {
invoice_id: invoiceId,
fiscal_id: null,
attempts: [],
}
}
const attempts = await this.prisma.saleInvoiceFiscalAttempts.findMany({
where: {
fiscal_id: invoice.fiscal.id,
},
orderBy: {
attempt_no: 'desc',
},
})
return {
invoice_id: invoiceId,
fiscal_id: invoice.fiscal.id,
attempts,
}
}
private async buildPayload(invoiceId: string, posInfo: IPosPayload) {
const invoice = await this.prisma.salesInvoice.findUnique({
where: {
id: invoiceId,
pos: {
id: posInfo.pos_id,
complex: {
business_activity_id: posInfo.business_id,
},
},
},
select: {
id: true,
code: true,
invoice_date: true,
total_amount: true,
items: {
select: {
id: true,
quantity: true,
unit_price: true,
total_amount: true,
measure_unit_text: true,
sku_code: true,
sku_vat: true,
measure_unit_code: true,
good_id: true,
service_id: true,
payload: true,
good_snapshot: true,
},
},
customer: {
select: {
type: true,
individual: true,
legal: true,
},
},
pos: {
select: {
complex: {
select: {
business_activity: {
select: {
fiscal_id: true,
economic_code: true,
consumer: {
select: {
legal: {
select: {
partner: {
select: {
fiscal_switch_type: true,
},
},
},
},
individual: {
select: {
partner: {
select: {
fiscal_switch_type: true,
},
},
},
},
},
},
},
},
},
},
},
},
},
})
if (!invoice) {
throw new NotFoundException('فاکتور مورد نظر یافت نشد.')
}
const { partner } = (invoice.pos.complex.business_activity.consumer.legal ||
invoice.pos.complex.business_activity.consumer.individual)!
return {
invoice_id: invoice.id,
invoice_code: invoice.code,
invoice_date: invoice.invoice_date,
total_amount: Number(invoice.total_amount),
customer_type: invoice.customer
? FiscalInvoiceCustomerType.Known
: FiscalInvoiceCustomerType.Unknown,
type: FiscalRequestType.MAIN,
provider_code: partner.fiscal_switch_type,
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),
good_id: item.good_id,
service_id: item.service_id,
payload: item.payload,
good_snapshot: item.good_snapshot,
discount: String((item.payload as Record<string, any>)?.discount || '0'),
sku: item.sku_code,
sku_vat: String(item.sku_vat),
measure_unit: item.measure_unit_code,
// sku: item.
})),
} satisfies TaxSwitchSendPayloadDto
}
private async safeSend(payload: TaxSwitchSendPayloadDto) {
try {
return await this.fiscalSwitchService.send(payload)
} catch (error: any) {
const now = new Date().toISOString()
return payload.items.map(item => ({
invoice_item_id: item.invoice_item_id,
status: TaxSendStatus.FAILED,
error_message: error?.message || 'Unexpected fiscal switch error.',
sent_at: now,
received_at: now,
}))
}
}
private async persistAttempt(
invoiceId: string,
itemResults: TaxSwitchSendItemResultDto[],
) {
return this.prisma.$transaction(async tx => {
const fiscal = await tx.saleInvoiceFiscals.upsert({
where: { invoice_id: invoiceId },
update: {},
create: {
invoice_id: invoiceId,
},
})
const latest = await tx.saleInvoiceFiscalAttempts.findFirst({
where: { fiscal_id: fiscal.id },
orderBy: { attempt_no: 'desc' },
select: { attempt_no: true },
})
const nextAttemptNo = (latest?.attempt_no || 0) + 1
const finalStatus = this.mapItemResultsStatus(itemResults)
const firstTaxId = itemResults.find(item => item.tax_id)?.tax_id || null
const sentAt = itemResults.map(item => item.sent_at).find(Boolean)
const receivedAt = itemResults.map(item => item.received_at).find(Boolean)
const errors = itemResults.map(item => item.error_message).filter(Boolean)
const errorMessage = errors.length ? Array.from(new Set(errors)).join(' | ') : null
const attempt = await tx.saleInvoiceFiscalAttempts.create({
data: {
fiscal_id: fiscal.id,
attempt_no: nextAttemptNo,
status: finalStatus,
tax_id: firstTaxId,
type: FiscalRequestType.MAIN,
request_payload: JSON.parse(
JSON.stringify(itemResults.map(item => item.request_payload)),
),
response_payload: JSON.parse(
JSON.stringify(itemResults.map(item => item.response_payload)),
),
error_message: errorMessage,
sent_at: sentAt ? new Date(sentAt) : null,
received_at: receivedAt ? new Date(receivedAt) : null,
},
})
await tx.saleInvoiceFiscals.update({
where: { id: fiscal.id },
data: {
retry_count: nextAttemptNo,
last_attempt_at: attempt.sent_at || attempt.created_at,
},
})
return {
fiscal_id: fiscal.id,
status: attempt.status,
attempt_no: attempt.attempt_no,
}
})
}
private mapItemResultsStatus(itemResults: TaxSwitchSendItemResultDto[]) {
if (!itemResults.length) {
return FiscalResponseStatus.NOT_SEND
}
const hasFailure = itemResults.some(item => item.status === TaxSendStatus.FAILED)
if (hasFailure) {
return FiscalResponseStatus.FAILURE
}
const hasSent = itemResults.some(item => item.status === TaxSendStatus.SENT)
if (hasSent) {
return FiscalResponseStatus.SUCCESS
}
return FiscalResponseStatus.NOT_SEND
}
private mapSwitchGetStatus(status: TaxSwitchGetResultDto['status']) {
switch (status) {
case TaxSendStatus.SENT:
return FiscalResponseStatus.SUCCESS
case TaxSendStatus.FAILED:
return FiscalResponseStatus.FAILURE
default:
return FiscalResponseStatus.NOT_SEND
}
}
private async getInvoiceForBusiness(invoiceId: string, businessId: string) {
const invoice = await this.prisma.salesInvoice.findFirst({
where: {
id: invoiceId,
pos: {
complex: {
business_activity_id: businessId,
},
},
},
select: {
id: true,
pos: {
select: {
provider: {
select: {
code: true,
},
},
},
},
fiscal: {
select: {
id: true,
retry_count: true,
last_attempt_at: true,
attempts: {
orderBy: {
attempt_no: 'desc',
},
take: 1,
select: {
id: true,
attempt_no: true,
status: true,
tax_id: true,
sent_at: true,
received_at: true,
error_message: true,
created_at: true,
},
},
},
},
},
})
if (!invoice) {
throw new NotFoundException('فاکتور مورد نظر یافت نشد.')
}
return invoice
}
}
@@ -1,8 +1,9 @@
import { Body, Controller, Get, Param, Post } from '@nestjs/common'
import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common'
import { PosInfo } from '@/common/decorators/posInfo.decorator'
import type { IPosPayload } from '@/common/models/posPayload.model'
import { CreateSalesInvoiceDto } from './dto/create-sales-invoice.dto'
import { SalesInvoicesFilterDto } from './dto/sales-invoices-filter.dto'
import { SalesInvoicesService } from './sales-invoices.service'
@Controller('pos/sales_invoices')
@@ -10,13 +11,13 @@ export class SalesInvoicesController {
constructor(private readonly salesInvoicesService: SalesInvoicesService) {}
@Get()
findAll() {
return this.salesInvoicesService.findAll()
findAll(@PosInfo() posInfo: IPosPayload, @Query() query: SalesInvoicesFilterDto) {
return this.salesInvoicesService.findAll(posInfo, query)
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.salesInvoicesService.findOne(+id)
findOne(@PosInfo() posInfo: IPosPayload, @Param('id') id: string) {
return this.salesInvoicesService.findOne(posInfo, id)
}
@Post()
@@ -2,10 +2,12 @@ import { Module } from '@nestjs/common'
import { SalesInvoiceFiscalSwitchService } from '../../consumer/saleInvoices/fiscal/sales-invoice-fiscal-switch.service'
import { SalesInvoiceFiscalService } from '../../consumer/saleInvoices/fiscal/sales-invoice-fiscal.service'
import { NamaTaxSwitchAdapter } from '../../consumer/saleInvoices/fiscal/switch/nama/nama-fiscal-switch.adapter'
import { PosSalesInvoiceFiscalModule } from './fiscal/sales-invoice-fiscal.module'
import { SalesInvoicesController } from './sales-invoices.controller'
import { SalesInvoicesService } from './sales-invoices.service'
@Module({
imports: [PosSalesInvoiceFiscalModule],
controllers: [SalesInvoicesController],
providers: [
SalesInvoicesService,
@@ -1,12 +1,18 @@
import { IPosPayload } from '@/common/models/posPayload.model'
import { translateEnumValue } from '@/common/utils'
import { PrismaService } from '@/prisma/prisma.service'
import { BadRequestException, Injectable } from '@nestjs/common'
import { ResponseMapper } from 'common/response/response-mapper'
import type { CustomerIndividual, CustomerLegal } from 'generated/prisma/client'
import { Prisma } from 'generated/prisma/client'
import { CustomerType, PaymentMethodType } from 'generated/prisma/enums'
import {
CustomerType,
FiscalResponseStatus,
PaymentMethodType,
} from 'generated/prisma/enums'
import { SalesInvoiceFiscalService } from '../../consumer/saleInvoices/fiscal/sales-invoice-fiscal.service'
import { CreateSalesInvoiceDto } from './dto/create-sales-invoice.dto'
import { SalesInvoicesFilterDto } from './dto/sales-invoices-filter.dto'
// Define type guard for CustomerIndividual
function isCustomerIndividual(customer: any): customer is CustomerIndividual {
@@ -50,14 +56,213 @@ export class SalesInvoicesService {
private salesInvoiceTaxService: SalesInvoiceFiscalService,
) {}
findAll() {
// TODO: Implement fetching all sales invoices
return []
async findAll(posInfo: IPosPayload, query: SalesInvoicesFilterDto = {}) {
const page = query.page || 1
const perPage = Math.min(query.perPage || 10, 100)
const where = this.buildFindAllWhere(posInfo, query)
const [items, total] = await this.prisma.$transaction(async tx => [
await tx.salesInvoice.findMany({
where,
orderBy: {
created_at: 'desc',
},
skip: (page - 1) * perPage,
take: perPage,
select: {
id: true,
code: true,
invoice_number: true,
invoice_date: true,
created_at: true,
total_amount: true,
customer: {
select: {
type: true,
individual: {
select: {
first_name: true,
last_name: true,
mobile_number: true,
national_id: true,
},
},
legal: {
select: {
name: true,
economic_code: true,
registration_number: true,
},
},
},
},
fiscal: {
select: {
attempts: {
orderBy: {
created_at: 'desc',
},
take: 1,
select: {
status: true,
},
},
},
},
},
}),
await tx.salesInvoice.count({ where }),
])
const summaryItems = items.map(invoice => ({
...invoice,
status: translateEnumValue(
'FiscalResponseStatus',
invoice.fiscal?.attempts?.[0]?.status || FiscalResponseStatus.NOT_SEND,
),
}))
return ResponseMapper.paginate(summaryItems, { total, page, perPage })
}
findOne(id: number) {
// TODO: Implement fetching a single sales invoice
return {}
async findOne(posInfo: IPosPayload, id: string) {
const invoice = await this.prisma.salesInvoice.findFirstOrThrow({
where: {
id,
pos: {
complex: {
business_activity_id: posInfo.business_id,
},
},
},
select: {
id: true,
code: true,
invoice_number: true,
invoice_date: true,
created_at: true,
updated_at: true,
notes: true,
total_amount: true,
unknown_customer: true,
customer: {
select: {
id: true,
type: true,
individual: {
select: {
first_name: true,
last_name: true,
mobile_number: true,
national_id: true,
postal_code: true,
economic_code: true,
},
},
legal: {
select: {
name: true,
economic_code: true,
registration_number: true,
postal_code: true,
},
},
},
},
pos: {
select: {
id: true,
name: true,
complex: {
select: {
id: true,
name: true,
business_activity: {
select: {
id: true,
name: true,
},
},
},
},
},
},
consumer_account: {
select: {
id: true,
role: true,
account: {
select: {
username: true,
},
},
},
},
items: {
select: {
id: true,
good_id: true,
service_id: true,
quantity: true,
measure_unit_code: true,
measure_unit_text: true,
sku_code: true,
unit_price: true,
discount: true,
total_amount: true,
notes: true,
payload: true,
good_snapshot: true,
},
},
payments: {
select: {
id: true,
amount: true,
payment_method: true,
paid_at: true,
created_at: true,
terminal_info: {
select: {
terminal_id: true,
stan: true,
rrn: true,
transaction_date_time: true,
customer_card_no: true,
description: true,
},
},
},
},
fiscal: {
select: {
id: true,
retry_count: true,
last_attempt_at: true,
attempts: {
orderBy: {
created_at: 'desc',
},
select: {
id: true,
attempt_no: true,
status: true,
tax_id: true,
error_message: true,
sent_at: true,
received_at: true,
created_at: true,
},
},
},
},
},
})
return ResponseMapper.single({
...invoice,
status: invoice.fiscal?.attempts?.[0]?.status || FiscalResponseStatus.NOT_SEND,
})
}
// async send(invoiceId: string, posInfo: IPosPayload) {
@@ -83,7 +288,7 @@ export class SalesInvoicesService {
// }
async create(data: CreateSalesInvoiceDto, posInfo: IPosPayload) {
const { business_id, pos_id, consumer_account_id } = posInfo
const { business_id, pos_id, consumer_account_id, complex_id } = posInfo
const normalizedInvoiceDate = this.normalizeInvoiceDate(data.invoice_date)
const { payments, terminalInfo } = this.buildPaymentsData(
data.payments,
@@ -101,6 +306,8 @@ export class SalesInvoicesService {
normalizedInvoiceDate,
invoiceNumber,
consumer_account_id,
businessId: business_id,
complexId: complex_id,
pos_id,
goodsById,
customerId: newCustomerId,
@@ -156,6 +363,161 @@ export class SalesInvoicesService {
)
}
private buildFindAllWhere(posInfo: IPosPayload, query: SalesInvoicesFilterDto) {
const where: Prisma.SalesInvoiceWhereInput = {
pos: {
complex: {
business_activity_id: posInfo.business_id,
},
},
}
if (query.code?.trim()) {
where.code = {
contains: query.code.trim(),
}
}
if (query.invoice_date_from || query.invoice_date_to) {
where.invoice_date = {
...(query.invoice_date_from ? { gte: new Date(query.invoice_date_from) } : {}),
...(query.invoice_date_to ? { lte: new Date(query.invoice_date_to) } : {}),
}
}
if (query.created_at_from || query.created_at_to) {
where.created_at = {
...(query.created_at_from ? { gte: new Date(query.created_at_from) } : {}),
...(query.created_at_to ? { lte: new Date(query.created_at_to) } : {}),
}
}
if (
query.total_amount !== undefined ||
query.total_amount_from !== undefined ||
query.total_amount_to !== undefined
) {
where.total_amount = {
...(query.total_amount !== undefined ? { equals: query.total_amount } : {}),
...(query.total_amount_from !== undefined
? { gte: query.total_amount_from }
: {}),
...(query.total_amount_to !== undefined ? { lte: query.total_amount_to } : {}),
}
}
if (
query.customer_name?.trim() ||
query.customer_mobile?.trim() ||
query.customer_national_id?.trim() ||
query.customer_economic_code?.trim()
) {
where.customer = {
is: {
OR: [
...(query.customer_name?.trim()
? [
{
individual: {
is: {
OR: [
{
first_name: {
contains: query.customer_name.trim(),
},
},
{
last_name: {
contains: query.customer_name.trim(),
},
},
],
},
},
},
{
legal: {
is: {
name: {
contains: query.customer_name.trim(),
},
},
},
},
]
: []),
...(query.customer_mobile?.trim()
? [
{
individual: {
is: {
mobile_number: {
contains: query.customer_mobile.trim(),
},
},
},
},
]
: []),
...(query.customer_national_id?.trim()
? [
{
individual: {
is: {
national_id: {
contains: query.customer_national_id.trim(),
},
},
},
},
]
: []),
...(query.customer_economic_code?.trim()
? [
{
legal: {
is: {
OR: [
{
economic_code: {
contains: query.customer_economic_code.trim(),
},
},
{
registration_number: {
contains: query.customer_economic_code.trim(),
},
},
],
},
},
},
]
: []),
],
},
}
}
if (query.status) {
if (query.status === FiscalResponseStatus.NOT_SEND) {
where.fiscal = null
} else {
where.fiscal = {
is: {
attempts: {
some: {
status: query.status,
},
},
},
}
}
}
return where
}
private normalizeInvoiceDate(invoiceDate: Date | string) {
return new Date(invoiceDate).toISOString() as any
}
@@ -339,11 +701,21 @@ export class SalesInvoicesService {
select: {
id: true,
name: true,
sku: true,
sku: {
select: {
id: true,
name: true,
},
},
local_sku: true,
barcode: true,
pricing_model: true,
unit_type: true,
measure_unit: {
select: {
id: true,
name: true,
},
},
base_sale_price: true,
image_url: true,
category: {
@@ -364,6 +736,8 @@ export class SalesInvoicesService {
normalizedInvoiceDate: Date
invoiceNumber: number
consumer_account_id: string
businessId: string
complexId: string
pos_id: string
goodsById: Map<string, any>
customerId: string | null
@@ -376,6 +750,8 @@ export class SalesInvoicesService {
pos_id,
goodsById,
customerId,
businessId,
complexId,
} = params
const { customer_id, customer_type, customer, payments, ...invoiceData } = data
@@ -384,7 +760,7 @@ export class SalesInvoicesService {
invoice_date: normalizedInvoiceDate,
invoice_number: invoiceNumber,
total_amount: data.total_amount,
code: 'INV-' + Date.now(),
code: this.generateInvoiceCode(businessId, complexId, pos_id, invoiceNumber),
items: {
createMany: {
data: data.items.map(item => ({
@@ -392,6 +768,7 @@ export class SalesInvoicesService {
quantity: item.quantity,
unit_price: item.unit_price,
total_amount: item.total_amount,
measure_unit_text: item.unit_type,
payload: item.payload ? JSON.parse(JSON.stringify(item.payload)) : undefined,
good_snapshot: item.good_id
? JSON.parse(
@@ -483,4 +860,13 @@ export class SalesInvoicesService {
}
}
}
private generateInvoiceCode(
business_id: string,
complex_id: string,
pos_id: string,
invoice_number: number,
) {
return `${business_id.substring(0, 2)}-${complex_id.substring(0, 2)}-${pos_id.substring(0, 2)}-${invoice_number.toString()}`
}
}