feat(cardex): implement cardex controller, service, and DTO for stock movements

feat(pos): add POS controller and service for order creation and stock retrieval
refactor(pos): enhance stock and product category retrieval with pagination and mapping
This commit is contained in:
2025-12-21 19:09:41 +03:30
parent d514267f58
commit c6a86719dd
42 changed files with 3769 additions and 119 deletions
+27
View File
@@ -0,0 +1,27 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'
import { ArrayMinSize, IsNumber, IsOptional } from 'class-validator'
export class CreateOrderDto {
@ApiProperty()
@IsNumber()
@IsOptional()
customerId?: number
@ApiPropertyOptional()
@ArrayMinSize(1)
items: CreateOrderItemDto[]
}
export class CreateOrderItemDto {
@ApiProperty()
@IsNumber()
productId: number
@ApiProperty()
@IsNumber()
count: number
@ApiProperty()
@IsNumber()
fee: number
}
+21
View File
@@ -0,0 +1,21 @@
import { ApiPropertyOptional } from '@nestjs/swagger'
import { Type } from 'class-transformer'
import { IsBoolean, IsOptional, IsString } from 'class-validator'
export class UpdateInventoryDto {
@ApiPropertyOptional()
@IsOptional()
@IsString()
name?: string
@ApiPropertyOptional()
@IsOptional()
@IsString()
location?: string
@ApiPropertyOptional()
@IsOptional()
@Type(() => Boolean)
@IsBoolean()
isActive?: boolean
}
+75
View File
@@ -0,0 +1,75 @@
import { Body, Controller, Get, Post } from '@nestjs/common'
import { CreateOrderDto } from './dto/create-pos.dto'
import { PosService } from './pos.service'
@Controller('pos')
export class PosController {
constructor(private readonly posService: PosService) {}
@Get()
getInfo() {
return this.posService.getInfo()
}
@Get('/stock')
async getStock() {
const inventoryId = await this.posService.getDefaultInventoryId()
return this.posService.getStock(inventoryId!)
}
@Get('/product-categories')
async getProductCategories() {
const inventoryId = await this.posService.getDefaultInventoryId()
return this.posService.getProductCategories(inventoryId!)
}
@Post('/orders/create')
async createOrder(@Body() dto: CreateOrderDto) {
const inventoryId = await this.posService.getDefaultInventoryId()
return this.posService.createOrder(dto, inventoryId!)
}
// @Post()
// create(@Body() dto: CreateInventoryDto) {
// return this.inventoriesService.create(dto)
// }
// @Get()
// @ApiQuery({ name: 'isPointOfSale', required: false, type: Boolean })
// findAll(@Query('isPointOfSale') isPointOfSale?: boolean) {
// return this.inventoriesService.findAll(isPointOfSale)
// }
// @Get(':id')
// findOne(@Param('id') id: string) {
// return this.inventoriesService.findOne(Number(id))
// }
// @Patch(':id')
// update(@Param('id') id: string, @Body() dto: UpdateInventoryDto) {
// return this.inventoriesService.update(Number(id), dto)
// }
// @Delete(':id')
// remove(@Param('id') id: string) {
// return this.inventoriesService.remove(Number(id))
// }
// @Get(':id/movements')
// @ApiQuery({ name: 'type', required: false, enum: MovementType })
// findInventoryMovements(@Param('id') id: string, @Query('type') type?: MovementType) {
// return this.inventoriesService.findInventoryMovements(Number(id), type ?? undefined)
// }
// @Get(':id/stock')
// @ApiQuery({ name: 'isAvailable', required: false, type: Boolean })
// getStock(@Param('id') id: string, @Query('isAvailable') isAvailable?: boolean) {
// return this.inventoriesService.getStock(Number(id), isAvailable ?? undefined)
// }
// @Get(':id/products/:productId/cardex')
// getProductCardex(@Param('id') id: string, @Param('productId') productId: string) {
// return this.inventoriesService.getProductCardex(Number(id), Number(productId))
// }
}
+11
View File
@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common'
import { PrismaModule } from '../../prisma/prisma.module'
import { PosController } from './pos.controller'
import { PosService } from './pos.service'
@Module({
imports: [PrismaModule],
controllers: [PosController],
providers: [PosService],
})
export class PosModule {}
+147
View File
@@ -0,0 +1,147 @@
import { Injectable } from '@nestjs/common'
import { ResponseMapper } from '../../common/response/response-mapper'
import { Prisma } from '../../generated/prisma/client'
import { PrismaService } from '../../prisma/prisma.service'
import { CreateOrderDto } from './dto/create-pos.dto'
@Injectable()
export class PosService {
constructor(private prisma: PrismaService) {}
async getDefaultInventoryId() {
const inventory = await this.prisma.inventory.findFirst({
where: { isPointOfSale: true },
select: { id: true },
})
return inventory?.id || null
}
async getInfo() {
const info = await this.prisma.inventory.findFirst({
where: { isPointOfSale: true },
select: {
id: true,
name: true,
},
})
return ResponseMapper.single({ store: info })
}
async getStock(inventoryId: number, isAvailable?: boolean, page = 1, pageSize = 50) {
const query: Prisma.StockBalanceFindManyArgs = {
where: {
inventoryId,
quantity:
isAvailable === true
? { gt: 0 }
: isAvailable === false
? { lte: 0 }
: undefined,
},
}
const [items, count] = await this.prisma.$transaction([
this.prisma.stockBalance.findMany({
where: query.where,
include: {
product: {
select: {
id: true,
name: true,
sku: true,
salePrice: true,
category: {
select: { id: true, name: true },
},
},
},
},
orderBy: {
quantity: 'desc',
},
skip: (page - 1) * pageSize,
take: pageSize,
}),
this.prisma.stockBalance.count({ where: query.where }),
])
const mapped = items.map(item => ({
id: item.id,
quantity: Number(item.quantity),
product: item.product,
}))
return ResponseMapper.paginate(mapped, count, page, pageSize)
}
async getProductCategories(inventoryId: number) {
const balances = await this.prisma.stockBalance.findMany({
where: { inventoryId },
select: {
product: { select: { id: true, category: { select: { id: true, name: true } } } },
quantity: true,
},
})
const map = new Map<
number,
{ id: number; name: string; totalQuantity: number; productCount: number }
>()
for (const b of balances) {
const product = b.product
const cat = product?.category
if (!cat) continue // skip products without category
const existing = map.get(cat.id)
if (existing) {
existing.totalQuantity += Number(b.quantity)
existing.productCount += 1
} else {
map.set(cat.id, {
id: cat.id,
name: cat.name,
totalQuantity: Number(b.quantity),
productCount: 1,
})
}
}
const categories = Array.from(map.values())
return ResponseMapper.list(categories)
}
async createOrder(data: CreateOrderDto, inventoryId: number) {
const { customerId, ...res } = data
const preparedOrder = { ...res } as any
const lastCode = await this.prisma.salesInvoice
.findFirst({
orderBy: { code: 'desc' },
select: { code: true },
})
.then(si => si?.code || '0')
preparedOrder.code = String(Number(lastCode) + 1)
preparedOrder.totalAmount = data.items.reduce(
(acc, item) => acc + Number(item.fee),
0,
)
preparedOrder.inventory = { connect: { id: inventoryId } }
preparedOrder.customer = { connect: { id: customerId } }
if (Object.prototype.hasOwnProperty.call(preparedOrder, 'items')) {
preparedOrder.items = {
create: preparedOrder.items.map((item: any) => ({
product: { connect: { id: Number(item.productId) } },
count: item.count,
fee: item.fee,
total: item.count * item.fee,
})),
}
}
const item = await this.prisma.salesInvoice.create({ data: preparedOrder })
return ResponseMapper.create(item)
}
}