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:
@@ -0,0 +1,27 @@
|
||||
import { Controller, Get, Query } from '@nestjs/common'
|
||||
import { ApiQuery } from '@nestjs/swagger'
|
||||
import { CardexService } from './cardex.service'
|
||||
|
||||
@Controller('cardex')
|
||||
export class CardexController {
|
||||
constructor(private readonly service: CardexService) {}
|
||||
|
||||
@Get('')
|
||||
@ApiQuery({ name: 'startDate', required: true, type: Date })
|
||||
@ApiQuery({ name: 'endDate', required: false, type: Date })
|
||||
@ApiQuery({ name: 'inventoryId', required: false, type: Number })
|
||||
@ApiQuery({ name: 'productId', required: false, type: Number })
|
||||
async getStock(
|
||||
@Query('startDate') startDate: Date,
|
||||
@Query('endDate') endDate: Date,
|
||||
@Query('inventoryId') inventoryId: number,
|
||||
@Query('productId') productId: number,
|
||||
) {
|
||||
return this.service.getCardex({
|
||||
startDate,
|
||||
endDate,
|
||||
inventoryId,
|
||||
productId,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Module } from '@nestjs/common'
|
||||
import { PrismaModule } from '../../prisma/prisma.module'
|
||||
import { CardexController } from './cardex.controller'
|
||||
import { CardexService } from './cardex.service'
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
controllers: [CardexController],
|
||||
providers: [CardexService],
|
||||
})
|
||||
export class CardexModule {}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { Injectable } from '@nestjs/common'
|
||||
import dayjs from 'dayjs'
|
||||
import { ResponseMapper } from '../../common/response/response-mapper'
|
||||
import { Prisma } from '../../generated/prisma/client'
|
||||
import { PrismaService } from '../../prisma/prisma.service'
|
||||
import { ReadCardexDto } from './dto/read-pos.dto'
|
||||
|
||||
@Injectable()
|
||||
export class CardexService {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
async getCardex({ startDate, endDate, inventoryId, productId }: ReadCardexDto) {
|
||||
if (!endDate) {
|
||||
endDate = dayjs(startDate).add(1, 'months').toDate()
|
||||
}
|
||||
const where: Prisma.StockMovementWhereInput = {}
|
||||
|
||||
if (inventoryId !== undefined) where.inventoryId = inventoryId
|
||||
if (productId !== undefined) where.productId = productId
|
||||
|
||||
if (startDate || endDate) {
|
||||
where.createdAt = {}
|
||||
if (startDate) where.createdAt.gte = new Date(startDate)
|
||||
if (endDate) where.createdAt.lte = new Date(endDate)
|
||||
}
|
||||
|
||||
const items = await this.prisma.stockMovement.findMany({
|
||||
where,
|
||||
include: { product: true, inventory: true },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
})
|
||||
|
||||
const mapped = items.map(i => ({
|
||||
id: i.id,
|
||||
type: i.type,
|
||||
quantity: Number(i.quantity),
|
||||
fee: Number(i.fee),
|
||||
totalCost: Number(i.totalCost),
|
||||
referenceType: i.referenceType,
|
||||
referenceId: i.referenceId,
|
||||
createdAt: i.createdAt,
|
||||
avgCost: Number(i.avgCost),
|
||||
product: {
|
||||
name: i.product.name,
|
||||
sku: i.product.sku,
|
||||
id: i.productId,
|
||||
},
|
||||
inventory: {
|
||||
name: i.inventory.name,
|
||||
id: i.inventoryId,
|
||||
},
|
||||
}))
|
||||
|
||||
return ResponseMapper.list(mapped)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger'
|
||||
import { IsDateString, IsNumber, IsOptional } from 'class-validator'
|
||||
|
||||
export class ReadCardexDto {
|
||||
@ApiPropertyOptional()
|
||||
@IsDateString()
|
||||
startDate: Date
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
endDate?: Date
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
inventoryId?: number
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
productId?: number
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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))
|
||||
// }
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user