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:
@@ -9,6 +9,7 @@ import { AdminGuildsModule } from './guilds/guilds.module'
|
||||
import { AdminLicensesModule } from './licenses/licenses.module'
|
||||
import { AdminPartnersModule } from './partners/partners.module'
|
||||
import { AdminProvidersModule } from './providers/providers.module'
|
||||
import { AdminStockKeepingUnitsModule } from './stock-keeping-units/stock-keeping-units.module'
|
||||
import { AdminTranslateModule } from './translate/translate.module'
|
||||
import { AdminUserAccountsModule } from './users/accounts/accounts.module'
|
||||
import { AdminUsersModule } from './users/users.module'
|
||||
@@ -25,6 +26,7 @@ import { AdminUsersModule } from './users/users.module'
|
||||
AdminGuildsModule,
|
||||
AdminDeviceBrandsModule,
|
||||
AdminDevicesModule,
|
||||
AdminStockKeepingUnitsModule,
|
||||
AdminLicensesModule,
|
||||
AdminConsumersModule,
|
||||
],
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
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'
|
||||
|
||||
export class CreateGuildGoodDto {
|
||||
@ApiProperty({ required: true })
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
name: string
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
sku: string
|
||||
|
||||
@IsString()
|
||||
@ApiProperty({ required: true })
|
||||
@IsString()
|
||||
sku_id: string
|
||||
|
||||
@ApiProperty({ required: true })
|
||||
@IsString()
|
||||
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 })
|
||||
|
||||
@@ -17,10 +17,20 @@ export class GoodsService {
|
||||
id: true,
|
||||
name: true,
|
||||
pricing_model: true,
|
||||
unit_type: true,
|
||||
image_url: true,
|
||||
sku: true,
|
||||
description: true,
|
||||
sku: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
measure_unit: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
category: {
|
||||
select: {
|
||||
id: true,
|
||||
@@ -62,7 +72,7 @@ export class GoodsService {
|
||||
}
|
||||
|
||||
async create(data: CreateGuildGoodDto, file: Express.Multer.File) {
|
||||
const { category_id, ...rest } = data
|
||||
const { category_id, measure_unit_id, sku_id, ...rest } = data
|
||||
|
||||
const good = await this.prisma.$transaction(async tx => {
|
||||
let image_url = ''
|
||||
@@ -79,6 +89,16 @@ export class GoodsService {
|
||||
...rest,
|
||||
is_default_guild_good: true,
|
||||
image_url,
|
||||
sku: {
|
||||
connect: {
|
||||
id: sku_id,
|
||||
},
|
||||
},
|
||||
measure_unit: {
|
||||
connect: {
|
||||
id: measure_unit_id,
|
||||
},
|
||||
},
|
||||
category: {
|
||||
connect: {
|
||||
id: category_id,
|
||||
@@ -93,7 +113,7 @@ export class GoodsService {
|
||||
}
|
||||
|
||||
async update(id: string, data: UpdateGuildGoodDto, file: Express.Multer.File) {
|
||||
const { category_id, ...rest } = data
|
||||
const { category_id, measure_unit_id, sku_id, ...rest } = data
|
||||
|
||||
const good = await this.prisma.$transaction(async tx => {
|
||||
let image_url = ''
|
||||
@@ -120,6 +140,16 @@ export class GoodsService {
|
||||
...rest,
|
||||
is_default_guild_good: true,
|
||||
image_url,
|
||||
sku: {
|
||||
connect: {
|
||||
id: sku_id,
|
||||
},
|
||||
},
|
||||
measure_unit: {
|
||||
connect: {
|
||||
id: measure_unit_id,
|
||||
},
|
||||
},
|
||||
category: {
|
||||
connect: {
|
||||
id: category_id,
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { SKUGuildType } from '@/generated/prisma/enums'
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'
|
||||
import { Type } from 'class-transformer'
|
||||
import { IsBoolean, IsEnum, IsNumber, IsString, Min } from 'class-validator'
|
||||
|
||||
export class CreateStockKeepingUnitDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
code: string
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
name: string
|
||||
|
||||
@ApiProperty()
|
||||
@Type(() => Number)
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
VAT: number
|
||||
|
||||
@ApiPropertyOptional({ enum: SKUGuildType, default: SKUGuildType.GOLD })
|
||||
@IsEnum(SKUGuildType)
|
||||
type: SKUGuildType = SKUGuildType.GOLD
|
||||
|
||||
@ApiPropertyOptional({ default: true })
|
||||
@IsBoolean()
|
||||
is_public: boolean = true
|
||||
|
||||
@ApiPropertyOptional({ default: true })
|
||||
@IsBoolean()
|
||||
is_domestic: boolean = true
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { StockKeepingUnitsService } from '../stock-keeping-units.service'
|
||||
|
||||
export type StockKeepingUnitsServiceCreateResponseDto = Awaited<
|
||||
ReturnType<StockKeepingUnitsService['create']>
|
||||
>
|
||||
export type StockKeepingUnitsServiceFindAllResponseDto = Awaited<
|
||||
ReturnType<StockKeepingUnitsService['findAll']>
|
||||
>
|
||||
export type StockKeepingUnitsServiceUpdateResponseDto = Awaited<
|
||||
ReturnType<StockKeepingUnitsService['update']>
|
||||
>
|
||||
@@ -0,0 +1,4 @@
|
||||
import { PartialType } from '@nestjs/swagger'
|
||||
import { CreateStockKeepingUnitDto } from './create-stock-keeping-unit.dto'
|
||||
|
||||
export class UpdateStockKeepingUnitDto extends PartialType(CreateStockKeepingUnitDto) {}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Body, Controller, Get, Param, Patch, Post } from '@nestjs/common'
|
||||
import { ApiTags } from '@nestjs/swagger'
|
||||
import type {
|
||||
StockKeepingUnitsServiceCreateResponseDto,
|
||||
StockKeepingUnitsServiceFindAllResponseDto,
|
||||
StockKeepingUnitsServiceUpdateResponseDto,
|
||||
} from './dto/stock-keeping-units-response.dto'
|
||||
import { CreateStockKeepingUnitDto } from './dto/create-stock-keeping-unit.dto'
|
||||
import { UpdateStockKeepingUnitDto } from './dto/update-stock-keeping-unit.dto'
|
||||
import { StockKeepingUnitsService } from './stock-keeping-units.service'
|
||||
|
||||
@ApiTags('AdminStockKeepingUnits')
|
||||
@Controller('admin/stock-keeping-units')
|
||||
export class StockKeepingUnitsController {
|
||||
constructor(private readonly service: StockKeepingUnitsService) {}
|
||||
|
||||
@Get()
|
||||
async findAll(): Promise<StockKeepingUnitsServiceFindAllResponseDto> {
|
||||
return this.service.findAll()
|
||||
}
|
||||
|
||||
@Post()
|
||||
async create(
|
||||
@Body() data: CreateStockKeepingUnitDto,
|
||||
): Promise<StockKeepingUnitsServiceCreateResponseDto> {
|
||||
return this.service.create(data)
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
async update(
|
||||
@Param('id') id: string,
|
||||
@Body() data: UpdateStockKeepingUnitDto,
|
||||
): Promise<StockKeepingUnitsServiceUpdateResponseDto> {
|
||||
return this.service.update(id, data)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { PrismaModule } from '@/prisma/prisma.module'
|
||||
import { Module } from '@nestjs/common'
|
||||
import { StockKeepingUnitsController } from './stock-keeping-units.controller'
|
||||
import { StockKeepingUnitsService } from './stock-keeping-units.service'
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
controllers: [StockKeepingUnitsController],
|
||||
providers: [StockKeepingUnitsService],
|
||||
})
|
||||
export class AdminStockKeepingUnitsModule {}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { PrismaService } from '@/prisma/prisma.service'
|
||||
import { Injectable } from '@nestjs/common'
|
||||
import { ResponseMapper } from 'common/response/response-mapper'
|
||||
import { CreateStockKeepingUnitDto } from './dto/create-stock-keeping-unit.dto'
|
||||
import { UpdateStockKeepingUnitDto } from './dto/update-stock-keeping-unit.dto'
|
||||
|
||||
@Injectable()
|
||||
export class StockKeepingUnitsService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async findAll() {
|
||||
const items = await this.prisma.stockKeepingUnits.findMany({
|
||||
orderBy: { created_at: 'desc' },
|
||||
})
|
||||
return ResponseMapper.list(items)
|
||||
}
|
||||
|
||||
async create(data: CreateStockKeepingUnitDto) {
|
||||
const item = await this.prisma.stockKeepingUnits.create({ data })
|
||||
return ResponseMapper.create(item)
|
||||
}
|
||||
|
||||
async update(id: string, data: UpdateStockKeepingUnitDto) {
|
||||
const item = await this.prisma.stockKeepingUnits.update({
|
||||
where: { id },
|
||||
data,
|
||||
})
|
||||
return ResponseMapper.update(item)
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,15 @@ import { PublicWithToken } from '@/common/decorators/withToken.decorator'
|
||||
import { Controller, Get, Param } from '@nestjs/common'
|
||||
import { ApiTags } from '@nestjs/swagger'
|
||||
import { CatalogsService } from './catalog.service'
|
||||
import type { CatalogsServiceGetDeviceBrandsResponseDto, CatalogsServiceGetDevicesResponseDto, CatalogsServiceGetGuildGoodCategoriesResponseDto, CatalogsServiceGetGuildsResponseDto, CatalogsServiceGetProvidersResponseDto } from './dto/catalog-response.dto'
|
||||
import type {
|
||||
CatalogsServiceGetDeviceBrandsResponseDto,
|
||||
CatalogsServiceGetDevicesResponseDto,
|
||||
CatalogsServiceGetGuildGoodCategoriesResponseDto,
|
||||
CatalogsServiceGetGuildsResponseDto,
|
||||
CatalogsServiceGetMeasurementsResponseDto,
|
||||
CatalogsServiceGetProvidersResponseDto,
|
||||
CatalogsServiceGetSKUResponseDto,
|
||||
} from './dto/catalog-response.dto'
|
||||
|
||||
@ApiTags('Catalog')
|
||||
@Controller('catalog')
|
||||
@@ -33,9 +41,27 @@ export class CatalogsController {
|
||||
return this.catalogService.getDeviceBrands()
|
||||
}
|
||||
|
||||
@Get('sku')
|
||||
@PublicWithToken()
|
||||
async getSKU(
|
||||
@Param('search') search: string,
|
||||
): Promise<CatalogsServiceGetSKUResponseDto> {
|
||||
return this.catalogService.getSKU(search)
|
||||
}
|
||||
|
||||
@Get('measure_units')
|
||||
@PublicWithToken()
|
||||
async getMeasurements(
|
||||
@Param('search') search: string,
|
||||
): Promise<CatalogsServiceGetMeasurementsResponseDto> {
|
||||
return this.catalogService.getMeasurements()
|
||||
}
|
||||
|
||||
@Get('guilds/:guildId/good_categories')
|
||||
@PublicWithToken()
|
||||
async getGuildGoodCategories(@Param('guildId') guild_id: string): Promise<CatalogsServiceGetGuildGoodCategoriesResponseDto> {
|
||||
async getGuildGoodCategories(
|
||||
@Param('guildId') guild_id: string,
|
||||
): Promise<CatalogsServiceGetGuildGoodCategoriesResponseDto> {
|
||||
return this.catalogService.getGuildGoodCategories(guild_id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,35 @@ export class CatalogsService {
|
||||
return ResponseMapper.list(items)
|
||||
}
|
||||
|
||||
async getMeasurements() {
|
||||
const items = await this.prisma.measureUnits.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
code: true,
|
||||
},
|
||||
})
|
||||
return ResponseMapper.list(items)
|
||||
}
|
||||
|
||||
async getSKU(search: string) {
|
||||
const items = await this.prisma.stockKeepingUnits.findMany({
|
||||
// where: {
|
||||
// OR: [{ code: { contains: search } }, { name: { contains: search } }],
|
||||
// },
|
||||
select: {
|
||||
id: true,
|
||||
code: true,
|
||||
name: true,
|
||||
is_domestic: true,
|
||||
is_public: true,
|
||||
VAT: true,
|
||||
},
|
||||
take: 100,
|
||||
})
|
||||
return ResponseMapper.list(items)
|
||||
}
|
||||
|
||||
async getGuildGoodCategories(guild_id: string) {
|
||||
const items = await this.prisma.goodCategory.findMany({
|
||||
where: {
|
||||
|
||||
@@ -1,7 +1,23 @@
|
||||
import type { CatalogsService } from '../catalog.service'
|
||||
|
||||
export type CatalogsServiceGetDeviceBrandsResponseDto = Awaited<ReturnType<CatalogsService['getDeviceBrands']>>
|
||||
export type CatalogsServiceGetDevicesResponseDto = Awaited<ReturnType<CatalogsService['getDevices']>>
|
||||
export type CatalogsServiceGetGuildGoodCategoriesResponseDto = Awaited<ReturnType<CatalogsService['getGuildGoodCategories']>>
|
||||
export type CatalogsServiceGetGuildsResponseDto = Awaited<ReturnType<CatalogsService['getGuilds']>>
|
||||
export type CatalogsServiceGetProvidersResponseDto = Awaited<ReturnType<CatalogsService['getProviders']>>
|
||||
export type CatalogsServiceGetDeviceBrandsResponseDto = Awaited<
|
||||
ReturnType<CatalogsService['getDeviceBrands']>
|
||||
>
|
||||
export type CatalogsServiceGetDevicesResponseDto = Awaited<
|
||||
ReturnType<CatalogsService['getDevices']>
|
||||
>
|
||||
export type CatalogsServiceGetGuildGoodCategoriesResponseDto = Awaited<
|
||||
ReturnType<CatalogsService['getGuildGoodCategories']>
|
||||
>
|
||||
export type CatalogsServiceGetGuildsResponseDto = Awaited<
|
||||
ReturnType<CatalogsService['getGuilds']>
|
||||
>
|
||||
export type CatalogsServiceGetProvidersResponseDto = Awaited<
|
||||
ReturnType<CatalogsService['getProviders']>
|
||||
>
|
||||
export type CatalogsServiceGetSKUResponseDto = Awaited<
|
||||
ReturnType<CatalogsService['getSKU']>
|
||||
>
|
||||
export type CatalogsServiceGetMeasurementsResponseDto = Awaited<
|
||||
ReturnType<CatalogsService['getMeasurements']>
|
||||
>
|
||||
|
||||
+15
-3
@@ -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,
|
||||
|
||||
+148
-19
@@ -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,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { PublicWithToken } from '@/common/decorators/withToken.decorator'
|
||||
import { Controller, Get } from '@nestjs/common'
|
||||
import { EnumsService } from './enums.service'
|
||||
import type { EnumsServiceGetEnumValuesResponseDto } from './dto/enums-response.dto'
|
||||
import { EnumsService } from './enums.service'
|
||||
|
||||
@Controller('enums')
|
||||
export class EnumsController {
|
||||
@@ -97,12 +97,6 @@ export class EnumsController {
|
||||
return this.enumsService.getEnumValues('TokenType')
|
||||
}
|
||||
|
||||
@Get('unit_type')
|
||||
@PublicWithToken()
|
||||
async getUnitType(): Promise<EnumsServiceGetEnumValuesResponseDto> {
|
||||
return this.enumsService.getEnumValues('UnitType')
|
||||
}
|
||||
|
||||
@Get('good_pricing_model')
|
||||
@PublicWithToken()
|
||||
async getGoodPricingModel(): Promise<EnumsServiceGetEnumValuesResponseDto> {
|
||||
@@ -114,4 +108,10 @@ export class EnumsController {
|
||||
async getConsumerRoleModel(): Promise<EnumsServiceGetEnumValuesResponseDto> {
|
||||
return this.enumsService.getEnumValues('ConsumerRole')
|
||||
}
|
||||
|
||||
@Get('fiscal_response_status')
|
||||
@PublicWithToken()
|
||||
async getFiscalResponseStatus(): Promise<EnumsServiceGetEnumValuesResponseDto> {
|
||||
return this.enumsService.getEnumValues('FiscalResponseStatus')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,14 +3,18 @@ import { translateEnumValue } from '@/common/utils/enum-translator.util'
|
||||
import {
|
||||
AccountRole,
|
||||
AccountStatus,
|
||||
AccountType,
|
||||
ApplicationPlatform,
|
||||
ApplicationPublisher,
|
||||
ApplicationReleaseType,
|
||||
BusinessRole,
|
||||
ComplexRole,
|
||||
ConsumerRole,
|
||||
ConsumerStatus,
|
||||
ConsumerType,
|
||||
CustomerType,
|
||||
FiscalInvoiceCustomerType,
|
||||
FiscalRequestType,
|
||||
FiscalResponseStatus,
|
||||
GoodPricingModel,
|
||||
LicenseStatus,
|
||||
@@ -24,12 +28,14 @@ import {
|
||||
POSType,
|
||||
ProviderRole,
|
||||
ProviderStatus,
|
||||
SKUGuildType,
|
||||
TokenType,
|
||||
UnitType,
|
||||
UserStatus,
|
||||
UserType,
|
||||
} from '@/generated/prisma/enums'
|
||||
import { Injectable } from '@nestjs/common'
|
||||
import { AccountType, GoldKarat, TokenType } from 'common/enums/enums'
|
||||
import { GoldKarat } from 'common/enums/enums'
|
||||
import { ResponseMapper } from 'common/response/response-mapper'
|
||||
|
||||
@Injectable()
|
||||
@@ -63,6 +69,7 @@ export class EnumsService {
|
||||
AccountType: this.prepareData(AccountType, 'AccountType'),
|
||||
PartnerRole: this.prepareData(PartnerRole, 'PartnerRole'),
|
||||
BusinessRole: this.prepareData(BusinessRole, 'BusinessRole'),
|
||||
ComplexRole: this.prepareData(ComplexRole, 'ComplexRole'),
|
||||
ProviderRole: this.prepareData(ProviderRole, 'ProviderRole'),
|
||||
ProviderStatus: this.prepareData(ProviderStatus, 'ProviderStatus'),
|
||||
TokenType: this.prepareData(TokenType, 'TokenType'),
|
||||
@@ -90,6 +97,12 @@ export class EnumsService {
|
||||
FiscalResponseStatus,
|
||||
'FiscalResponseStatus',
|
||||
),
|
||||
FiscalRequestType: this.prepareData(FiscalRequestType, 'FiscalRequestType'),
|
||||
FiscalInvoiceCustomerType: this.prepareData(
|
||||
FiscalInvoiceCustomerType,
|
||||
'FiscalInvoiceCustomerType',
|
||||
),
|
||||
SKUGuildType: this.prepareData(SKUGuildType, 'SKUGuildType'),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -114,6 +114,7 @@ export class BusinessActivitiesService {
|
||||
const createdBusinessActivity = await tx.businessActivity.create({
|
||||
data: {
|
||||
...rest,
|
||||
economic_code: String(data.economic_code),
|
||||
guild: {
|
||||
connect: {
|
||||
id: guild_id,
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
@@ -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()}`
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user