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
@@ -35,7 +35,9 @@ export class SalesInvoicesService {
items: {
select: {
unit_type: true,
measure_unit_code: true,
measure_unit_text: true,
sku_code: true,
discount: true,
notes: true,
quantity: true,
@@ -46,11 +48,21 @@ export class SalesInvoicesService {
select: {
id: true,
name: true,
sku: true,
sku: {
select: {
id: true,
name: true,
},
},
barcode: true,
local_sku: true,
pricing_model: true,
unit_type: true,
measure_unit: {
select: {
id: true,
name: true,
},
},
category: {
select: {
id: true,
@@ -1,4 +1,4 @@
import { GoodPricingModel, UnitType } from '@/generated/prisma/enums'
import { GoodPricingModel } from '@/generated/prisma/enums'
import { ApiProperty, PartialType } from '@nestjs/swagger'
import { IsEnum, IsOptional, IsString } from 'class-validator'
@@ -9,15 +9,15 @@ export class CreateGoodDto {
@IsString()
@ApiProperty({ required: true })
sku: string
sku_id: string
@IsString()
@ApiProperty({ required: true })
category_id: string
@IsEnum(UnitType)
@ApiProperty({ required: true, enum: UnitType })
unit_type: UnitType
@ApiProperty({ required: true })
@IsString()
measure_unit_id: string
@IsEnum(GoodPricingModel)
@ApiProperty({ required: true, enum: GoodPricingModel })
@@ -12,11 +12,21 @@ export class ConsumerBusinessActivityGoodsService {
id: true,
name: true,
barcode: true,
sku: true,
local_sku: true,
pricing_model: true,
unit_type: true,
image_url: true,
sku: {
select: {
id: true,
name: true,
},
},
measure_unit: {
select: {
id: true,
name: true,
},
},
category: {
select: {
id: true,
@@ -55,7 +65,7 @@ export class ConsumerBusinessActivityGoodsService {
}
async create(business_activity_id: string, data: CreateGoodDto) {
const { category_id, ...rest } = data
const { category_id, sku_id, measure_unit_id, ...rest } = data
const good = await this.prisma.good.create({
data: {
@@ -70,6 +80,16 @@ export class ConsumerBusinessActivityGoodsService {
id: category_id,
},
},
sku: {
connect: {
id: sku_id,
},
},
measure_unit: {
connect: {
id: measure_unit_id,
},
},
},
})
return ResponseMapper.create(good)
@@ -81,7 +101,7 @@ export class ConsumerBusinessActivityGoodsService {
id: string,
data: UpdateGoodDto,
) {
const { category_id, ...rest } = data
const { category_id, sku_id, measure_unit_id, ...rest } = data
const good = await this.prisma.good.update({
where: {
business_activity: {
@@ -101,6 +121,16 @@ export class ConsumerBusinessActivityGoodsService {
id: category_id,
},
},
sku: {
connect: {
id: sku_id,
},
},
measure_unit: {
connect: {
id: measure_unit_id,
},
},
...rest,
},
})
@@ -27,7 +27,7 @@ export class consumerCustomersService {
},
legal: {
select: {
company_name: true,
name: true,
registration_number: true,
postal_code: true,
economic_code: true,
@@ -132,7 +132,12 @@ export class CustomerSaleInvoicesService {
id: true,
name: true,
pricing_model: true,
unit_type: true,
measure_unit: {
select: {
id: true,
name: true,
},
},
category: {
select: {
id: true,
@@ -1,3 +1,9 @@
import {
CustomerType,
FiscalInvoiceCustomerType,
FiscalRequestType,
PartnerFiscalSwitchType,
} from '@/generated/prisma/enums'
import { ApiProperty } from '@nestjs/swagger'
import { Type } from 'class-transformer'
import {
@@ -17,7 +23,67 @@ export enum TaxSendStatus {
PENDING = 'PENDING',
}
export class CustomerLegalInfoDto {
@ApiProperty({ required: true })
@IsString()
name: string
@ApiProperty({ required: true })
@IsString()
economic_code: string
}
export class CustomerIndividualInfoDto {
@ApiProperty({ required: true })
@IsString()
first_name: string
@ApiProperty({ required: true })
@IsString()
last_name: string
@ApiProperty({ required: true })
@IsString()
national_id: string
@ApiProperty({ required: true })
@IsString()
mobile_number: string
}
export class CustomerInfoDto {
@ApiProperty({ required: true, enum: CustomerType })
@IsEnum(CustomerType)
type: CustomerType
@ApiProperty({})
@Type(() => CustomerLegalInfoDto)
@ValidateNested({ each: true })
legal_info?: CustomerLegalInfoDto
@ApiProperty({})
@Type(() => CustomerIndividualInfoDto)
@ValidateNested({ each: true })
individual_info?: CustomerIndividualInfoDto
}
export class TaxSwitchSendItemPayloadDto {
@ApiProperty({ required: true })
@IsString()
sku: string
@ApiProperty({ required: true })
@IsString()
sku_vat: string
@ApiProperty()
@IsOptional()
@IsString()
discount: string
@ApiProperty({ required: true })
@IsNumber()
unit_price: number
@ApiProperty()
@IsString()
invoice_item_id: string
@@ -26,17 +92,13 @@ export class TaxSwitchSendItemPayloadDto {
@IsNumber()
quantity: number
@ApiProperty()
@IsNumber()
unit_price: number
@ApiProperty()
@IsNumber()
total_amount: number
@ApiProperty()
@ApiProperty({ required: true })
@IsString()
unit_type: string
measure_unit: string
@ApiProperty({ required: false, nullable: true })
@IsOptional()
@@ -52,41 +114,48 @@ export class TaxSwitchSendItemPayloadDto {
@IsOptional()
@IsObject()
payload?: unknown
@ApiProperty({ required: false, nullable: true, type: Object })
@IsOptional()
@IsObject()
good_snapshot?: unknown
}
export class TaxSwitchSendPayloadDto {
@ApiProperty()
@IsString()
invoice_id: string
@ApiProperty({ required: true, enum: FiscalRequestType })
@IsEnum(FiscalRequestType)
type: FiscalRequestType
@ApiProperty()
@IsString()
invoice_code: string
@ApiProperty({ required: false, nullable: true })
@IsOptional()
@IsDateString()
invoice_date: string | null
@ApiProperty()
@IsNumber()
total_amount: number
@ApiProperty({ required: false, nullable: true })
@IsOptional()
@IsString()
provider_code?: string | null
@ApiProperty({ required: true, enum: FiscalInvoiceCustomerType })
@IsEnum(FiscalInvoiceCustomerType)
customer_type: FiscalInvoiceCustomerType
@ApiProperty({ type: [TaxSwitchSendItemPayloadDto] })
@IsArray()
@ValidateNested({ each: true })
@Type(() => TaxSwitchSendItemPayloadDto)
items: TaxSwitchSendItemPayloadDto[]
@ApiProperty({ required: false })
@Type(() => CustomerInfoDto)
@ValidateNested({ each: true })
customer?: CustomerInfoDto
@ApiProperty({ required: true, nullable: true })
@IsString()
invoice_code: string
@ApiProperty({ required: true, nullable: true })
@IsOptional()
@IsDateString()
invoice_date: Date
@ApiProperty({ required: true, nullable: true })
@IsString()
invoice_id: string
@ApiProperty()
@IsNumber()
total_amount: number
@ApiProperty({ required: true, enum: PartnerFiscalSwitchType })
@IsEnum(PartnerFiscalSwitchType)
provider_code: PartnerFiscalSwitchType
}
export class TaxSwitchSendItemResultDto {
@@ -1,6 +1,10 @@
import { PrismaService } from '@/prisma/prisma.service'
import { Injectable, NotFoundException } from '@nestjs/common'
import { Prisma } from 'generated/prisma/client'
import {
FiscalInvoiceCustomerType,
FiscalRequestType,
Prisma,
} from 'generated/prisma/client'
import {
TaxSendStatus,
TaxSwitchGetResultDto,
@@ -137,7 +141,7 @@ export class SalesInvoiceFiscalService {
invoiceId: string,
posId: string,
): Promise<TaxSwitchSendPayloadDto> {
const invoice = await this.prisma.salesInvoice.findFirst({
const invoice = await this.prisma.salesInvoice.findUnique({
where: {
id: invoiceId,
pos_id: posId,
@@ -153,7 +157,10 @@ export class SalesInvoiceFiscalService {
quantity: true,
unit_price: true,
total_amount: true,
unit_type: true,
measure_unit_code: true,
measure_unit_text: true,
sku_code: true,
sku_vat: true,
good_id: true,
service_id: true,
payload: true,
@@ -162,9 +169,55 @@ export class SalesInvoiceFiscalService {
},
pos: {
select: {
provider: {
complex: {
select: {
code: true,
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,
},
},
},
},
},
},
},
},
},
},
},
},
customer: {
select: {
type: true,
individual: {
select: {
first_name: true,
last_name: true,
national_id: true,
mobile_number: true,
},
},
legal: {
select: {
name: true,
economic_code: true,
},
},
},
@@ -176,18 +229,36 @@ export class SalesInvoiceFiscalService {
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 ? invoice.invoice_date.toISOString() : null,
invoice_date: invoice.invoice_date,
total_amount: Number(invoice.total_amount),
provider_code: invoice.pos.provider?.code || null,
// provider_code: invoice.pos.provider?.code || null,
type: FiscalRequestType.MAIN,
provider_code: partner.fiscal_switch_type!,
customer_type: invoice.customer?.type
? FiscalInvoiceCustomerType.Known
: FiscalInvoiceCustomerType.Unknown,
customer: invoice.customer?.type
? {
type: invoice.customer.type,
legal_info: invoice.customer.legal ?? undefined,
individual_info: invoice.customer.individual ?? undefined,
}
: undefined,
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),
unit_type: item.unit_type,
measure_unit: item.measure_unit_code,
sku: item.sku_code,
sku_vat: String(item.sku_vat),
discount: String((item.payload as Record<string, any>)?.discount || '0'),
good_id: item.good_id,
service_id: item.service_id,
payload: item.payload,
@@ -1,3 +1,8 @@
import {
CustomerType,
FiscalInvoiceCustomerType,
FiscalRequestType,
} from '@/generated/prisma/enums'
import { Injectable } from '@nestjs/common'
import {
ITaxSwitchAdapter,
@@ -7,36 +12,74 @@ import {
TaxSwitchSendItemResultDto,
TaxSwitchSendPayloadDto,
} from '../../dto/tax-switch.dto'
import {
NamaTaxGetResponseDto,
NamaTaxRequestDto,
NamaTaxSendItemResponseDto,
} from './nama-fiscal-switch.dto'
@Injectable()
export class NamaTaxSwitchAdapter implements ITaxSwitchAdapter {
readonly code = 'NAMA'
private readonly sandboxBaseUrl =
process.env.NAMA_FISCAL_SANDBOX_URL || 'https://sandbox-api.nama.ir'
private readonly productionBaseUrl =
process.env.NAMA_FISCAL_PRODUCTION_URL || 'https://api.nama.ir'
private readonly sendPath = '/api/v1/fiscal/invoices/single-send'
private readonly sendBulkPath = '/api/v1/invoices'
private readonly checkStatus = '/api/v1/invoices/check-by-uuids'
private readonly checkBulkStatus = '/api/v1/invoices/check-by-uuids'
private readonly getPath = '/api/v1/fiscal/invoices/'
private get baseUrl() {
return process.env.NODE_ENV === 'production'
? this.productionBaseUrl
: this.sandboxBaseUrl
}
private buildSendUrl() {
return `${this.baseUrl}${this.sendPath}`
}
private buildGetUrl(taxId: string) {
return `${this.baseUrl}${this.getPath}/${taxId}`
}
async send(payload: TaxSwitchSendPayloadDto): Promise<TaxSwitchSendItemResultDto[]> {
const mappedRequest = this.mapToNamaRequestDto(payload)
return payload.items.map((item, index) => {
const sentAt = new Date()
const receivedAt = new Date(sentAt.getTime() + 50)
const externalRequest = {
invoiceCode: payload.invoice_code,
itemId: item.invoice_item_id,
quantity: item.quantity,
unitPrice: item.unit_price,
totalAmount: item.total_amount,
}
const externalResponse = {
status: 'ACCEPTED',
trackingCode: `TAX-${payload.invoice_code}-${index + 1}`,
const providerResponse: NamaTaxSendItemResponseDto = {
invoice_item_id: item.invoice_item_id,
status: TaxSendStatus.SENT,
tax_id: `TAX-${payload.invoice_code}-${index + 1}`,
request_payload: {
url: this.buildSendUrl(),
body: mappedRequest,
},
response_payload: {
provider: this.code,
env: process.env.NODE_ENV || 'development',
endpoint: this.buildSendUrl(),
status: 'ACCEPTED',
},
sent_at: sentAt.toISOString(),
received_at: receivedAt.toISOString(),
}
return {
invoice_item_id: item.invoice_item_id,
status: TaxSendStatus.SENT,
tax_id: externalResponse.trackingCode,
request_payload: externalRequest,
response_payload: externalResponse,
sent_at: sentAt.toISOString(),
received_at: receivedAt.toISOString(),
invoice_item_id: providerResponse.invoice_item_id,
status: providerResponse.status,
tax_id: providerResponse.tax_id,
request_payload: providerResponse.request_payload,
response_payload: providerResponse.response_payload,
error_message: providerResponse.error_message,
sent_at: providerResponse.sent_at,
received_at: providerResponse.received_at,
}
})
}
@@ -56,14 +99,100 @@ export class NamaTaxSwitchAdapter implements ITaxSwitchAdapter {
}
async get(taxId: string): Promise<TaxSwitchGetResultDto> {
return {
const providerResponse: NamaTaxGetResponseDto = {
tax_id: taxId,
status: TaxSendStatus.SENT,
response_payload: {
provider: this.code,
env: process.env.NODE_ENV || 'development',
endpoint: this.buildGetUrl(taxId),
status: 'CONFIRMED',
trackingCode: taxId,
tracking_code: taxId,
},
received_at: new Date().toISOString(),
}
return {
tax_id: providerResponse.tax_id,
status: providerResponse.status,
response_payload: providerResponse.response_payload,
received_at: providerResponse.received_at,
}
}
private mapToNamaRequestDto(payload: TaxSwitchSendPayloadDto): NamaTaxRequestDto {
return {
uuid: payload.invoice_id,
economic_code: '',
fiscal_id: '',
payment: [],
header: {
ins: this.mapFiscalRequestType(payload.type),
inp: '2',
inty: this.mapFiscalInvoiceCustomerType(payload.customer_type),
// unique_tax_code: payload.invoice_code,
inno: payload.invoice_code,
tins: '',
indatim: payload.invoice_date.getTime() + '',
name:
`${payload.customer?.individual_info?.first_name} ${payload.customer?.individual_info?.last_name}` ||
payload.customer?.legal_info?.name ||
'',
tob: this.mapCustomerType(payload.customer?.type),
address: '',
mobile: payload.customer?.individual_info?.mobile_number || '',
bid:
payload.customer?.individual_info?.national_id ||
payload.customer?.legal_info?.economic_code ||
'',
},
body: payload.items.map(item => ({
sstid: item.sku,
vra: item.sku_vat,
// sstt: item.unit_type,
fee: String(item.unit_price),
dis: String(item.discount),
mu: item.measure_unit,
am: String(item.quantity),
consfee: '0',
bros: '0',
spro: '0',
})),
}
}
private mapFiscalRequestType(type: FiscalRequestType) {
switch (type) {
case FiscalRequestType.MAIN:
return '1'
case FiscalRequestType.UPDATE:
return '2'
case FiscalRequestType.REVOKE:
return '3'
case FiscalRequestType.REMOVE:
return '4'
}
}
private mapFiscalInvoiceCustomerType(type?: FiscalInvoiceCustomerType) {
switch (type) {
case FiscalInvoiceCustomerType.Unknown:
return '1'
case FiscalInvoiceCustomerType.Known:
return '2'
default:
return '3'
}
}
private mapCustomerType(type?: CustomerType) {
switch (type) {
case CustomerType.INDIVIDUAL:
return '1'
case CustomerType.LEGAL:
return '2'
default:
return ''
}
}
}
@@ -1,6 +1,14 @@
import { ApiProperty } from '@nestjs/swagger'
import { Type } from 'class-transformer'
import { IsArray, IsString, ValidateNested } from 'class-validator'
import {
IsArray,
IsEnum,
IsObject,
IsOptional,
IsString,
ValidateNested,
} from 'class-validator'
import { TaxSendStatus } from '../../dto/tax-switch.dto'
export class NamaTaxBodyItemDto {
@ApiProperty()
@@ -11,9 +19,9 @@ export class NamaTaxBodyItemDto {
@IsString()
vra: string
@ApiProperty()
@IsString()
sstt: string
// @ApiProperty()
// @IsString()
// sstt: string
@ApiProperty()
@IsString()
@@ -45,7 +53,7 @@ export class NamaTaxBodyItemDto {
}
export class NamaTaxHeaderDto {
@ApiProperty()
@ApiProperty({ required: true })
@IsString()
ins: string
@@ -53,41 +61,43 @@ export class NamaTaxHeaderDto {
@IsString()
inp: string
@ApiProperty()
@ApiProperty({ required: true })
@IsString()
inty: string
@ApiProperty()
@IsString()
unique_tax_code: string
// @ApiProperty({required: true})
// @IsString()
// unique_tax_code: string
@ApiProperty()
@ApiProperty({ required: true })
@IsString()
inno: string
@ApiProperty()
@ApiProperty({ required: true })
@IsString()
tins: string
@ApiProperty()
@ApiProperty({ required: true })
@IsString()
indatim: string
@ApiProperty()
@ApiProperty({ required: true })
@IsString()
name: string
@ApiProperty()
@ApiProperty({ required: true })
@IsString()
tob: string
@ApiProperty()
@IsOptional()
@IsString()
address: string
address?: string
@ApiProperty()
@IsOptional()
@IsString()
mobile: string
mobile?: string
@ApiProperty()
@IsString()
@@ -122,3 +132,63 @@ export class NamaTaxRequestDto {
@IsString()
fiscal_id: string
}
export class NamaTaxSendItemResponseDto {
@ApiProperty()
@IsString()
invoice_item_id: string
@ApiProperty({ enum: TaxSendStatus })
@IsEnum(TaxSendStatus)
status: TaxSendStatus
@ApiProperty({ required: false, nullable: true })
@IsOptional()
@IsString()
tax_id?: string | null
@ApiProperty({ required: false, nullable: true, type: Object })
@IsOptional()
@IsObject()
request_payload?: unknown
@ApiProperty({ required: false, nullable: true, type: Object })
@IsOptional()
@IsObject()
response_payload?: unknown
@ApiProperty({ required: false, nullable: true })
@IsOptional()
@IsString()
error_message?: string | null
@ApiProperty({ required: false, nullable: true })
@IsOptional()
@IsString()
sent_at?: string | null
@ApiProperty({ required: false, nullable: true })
@IsOptional()
@IsString()
received_at?: string | null
}
export class NamaTaxGetResponseDto {
@ApiProperty()
@IsString()
tax_id: string
@ApiProperty({ enum: TaxSendStatus })
@IsEnum(TaxSendStatus)
status: TaxSendStatus
@ApiProperty({ required: false, nullable: true, type: Object })
@IsOptional()
@IsObject()
response_payload?: unknown
@ApiProperty({ required: false, nullable: true })
@IsOptional()
@IsString()
received_at?: string | null
}
@@ -117,7 +117,12 @@ export class SaleInvoicesService {
id: true,
name: true,
pricing_model: true,
unit_type: true,
measure_unit: {
select: {
id: true,
name: true,
},
},
category: {
select: {
@@ -136,7 +141,7 @@ export class SaleInvoicesService {
type: true,
legal: {
select: {
company_name: true,
name: true,
economic_code: true,
registration_number: true,
postal_code: true,