feat: implement POS order management system
- Added CreateOrderDto and CreateOrderItemDto for order creation. - Developed PosOrdersController to handle order-related endpoints. - Created PosOrdersService for business logic related to orders. - Implemented PosOrdersWorkflow for order processing workflows. - Established Prisma integration for database operations in orders. - Introduced SalesInvoicesModule and related DTOs for sales invoice management. - Enhanced SalesInvoicesService and SalesInvoicesWorkflow for invoice processing.
This commit is contained in:
+1
-1
@@ -2,7 +2,7 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'
|
||||
import { Type } from 'class-transformer'
|
||||
import { ArrayMinSize, IsInt, IsNumber, IsOptional, IsString } from 'class-validator'
|
||||
|
||||
export class CreateSaleInvoiceDto {
|
||||
export class CreateOrderDto {
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@@ -0,0 +1,44 @@
|
||||
import { Body, Controller, Get, Param, Post } from '@nestjs/common'
|
||||
import { OrderStatus } from '../../../generated/prisma/enums'
|
||||
import { CreateOrderDto } from './dto/create-order-item.dto'
|
||||
import { PosOrdersService } from './orders.service'
|
||||
|
||||
@Controller('pos/:posId/orders')
|
||||
export class PosOrdersController {
|
||||
constructor(private readonly posOrdersService: PosOrdersService) {}
|
||||
|
||||
@Get('')
|
||||
get(@Param('posId') posId: string) {
|
||||
return this.posOrdersService.getOrders(Number(posId))
|
||||
}
|
||||
|
||||
@Get('/held')
|
||||
getHeld(@Param('posId') posId: string) {
|
||||
return this.posOrdersService.getOrders(Number(posId), OrderStatus.PENDING)
|
||||
}
|
||||
|
||||
@Post('')
|
||||
async createOrder(@Param('posId') posId: string, @Body() dto: CreateOrderDto) {
|
||||
return this.posOrdersService.createOrder(Number(posId), dto)
|
||||
}
|
||||
|
||||
@Get('/:orderId')
|
||||
getOrderDetails(@Param('posId') posId: string, @Param('orderId') orderId: string) {
|
||||
return this.posOrdersService.getOrderDetails(Number(posId), Number(orderId))
|
||||
}
|
||||
|
||||
@Post('/:orderId/reject')
|
||||
async rejectOrder(@Param('posId') posId: string, @Param('orderId') orderId: string) {
|
||||
return this.posOrdersService.rejectOrder(Number(posId), Number(orderId))
|
||||
}
|
||||
|
||||
@Post('/:orderId/done')
|
||||
async completeOrder(@Param('posId') posId: string, @Param('orderId') orderId: string) {
|
||||
return this.posOrdersService.completeOrder(Number(posId), Number(orderId))
|
||||
}
|
||||
|
||||
@Post('/:orderId/cancel')
|
||||
async cancelOrder(@Param('posId') posId: string, @Param('orderId') orderId: string) {
|
||||
return this.posOrdersService.cancelOrder(Number(posId), Number(orderId))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Module } from '@nestjs/common'
|
||||
import { PrismaModule } from '../../../prisma/prisma.module'
|
||||
import { SalesInvoicesModule } from '../../sales-invoices/sales-invoices.module'
|
||||
import { PosOrdersController } from './orders.controller'
|
||||
import { PosOrdersService } from './orders.service'
|
||||
import { PosOrdersWorkflow } from './orders.workflow'
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, SalesInvoicesModule],
|
||||
controllers: [PosOrdersController],
|
||||
providers: [PosOrdersService, PosOrdersWorkflow],
|
||||
})
|
||||
export class PosOrdersModule {}
|
||||
@@ -0,0 +1,145 @@
|
||||
import { Injectable } from '@nestjs/common'
|
||||
import { ResponseMapper } from '../../../common/response/response-mapper'
|
||||
import { Prisma } from '../../../generated/prisma/client'
|
||||
import { OrderStatus } from '../../../generated/prisma/enums'
|
||||
import { OrderCreateInput } from '../../../generated/prisma/models'
|
||||
import { PrismaService } from '../../../prisma/prisma.service'
|
||||
import { CreateOrderDto } from './dto/create-order-item.dto'
|
||||
import { PosOrdersWorkflow } from './orders.workflow'
|
||||
|
||||
@Injectable()
|
||||
export class PosOrdersService {
|
||||
constructor(
|
||||
private prisma: PrismaService,
|
||||
private posWorkflow: PosOrdersWorkflow,
|
||||
) {}
|
||||
|
||||
async createOrder(posId: number, data: CreateOrderDto) {
|
||||
const { customerId, items, ...rest } = data
|
||||
const lastCode = await this.prisma.order
|
||||
.findFirst({
|
||||
where: { posAccountId: posId },
|
||||
orderBy: { orderNumber: 'desc' },
|
||||
select: { orderNumber: true },
|
||||
})
|
||||
.then(si => si?.orderNumber || '0')
|
||||
|
||||
const newCode = String(Number(lastCode) + 1)
|
||||
|
||||
const totalAmount = data.items.reduce(
|
||||
(acc, item) => acc + item.count * item.unitPrice,
|
||||
0,
|
||||
)
|
||||
|
||||
const preparedOrder: OrderCreateInput = {
|
||||
...rest,
|
||||
orderNumber: newCode,
|
||||
posAccount: { connect: { id: posId } },
|
||||
customer: customerId ? { connect: { id: customerId } } : undefined,
|
||||
totalAmount,
|
||||
orderItems: {
|
||||
create: items.map(item => ({
|
||||
product: { connect: { id: Number(item.productId) } },
|
||||
quantity: item.count,
|
||||
unitPrice: item.unitPrice,
|
||||
totalAmount: item.count * item.unitPrice,
|
||||
})),
|
||||
},
|
||||
}
|
||||
|
||||
const item = await this.prisma.$transaction(async tx => {
|
||||
const order = await tx.order.create({ data: preparedOrder })
|
||||
|
||||
await this.posWorkflow.onCreateOrder(tx, order.id, posId)
|
||||
})
|
||||
return ResponseMapper.create(item)
|
||||
}
|
||||
|
||||
async createAndConfirmOrder(posId: number, data: CreateOrderDto) {}
|
||||
|
||||
async rejectOrder(posAccountId: number, orderId: number) {
|
||||
const item = await this.prisma.$transaction(async tx => {
|
||||
const order = await tx.order.update({
|
||||
where: { id: orderId, posAccountId },
|
||||
data: { status: OrderStatus.REJECTED },
|
||||
})
|
||||
await this.posWorkflow.onRejectOrder(tx, orderId)
|
||||
return order
|
||||
})
|
||||
return ResponseMapper.single(item)
|
||||
}
|
||||
|
||||
async completeOrder(posAccountId: number, orderId: number) {
|
||||
const item = await this.prisma.$transaction(async tx => {
|
||||
const order = await tx.order.update({
|
||||
where: { id: orderId, posAccountId },
|
||||
data: { status: OrderStatus.DONE },
|
||||
})
|
||||
await this.posWorkflow.onConfirmOrder(tx, orderId)
|
||||
return order
|
||||
})
|
||||
return ResponseMapper.single(item)
|
||||
}
|
||||
|
||||
async cancelOrder(posAccountId: number, orderId: number) {
|
||||
const item = await this.prisma.$transaction(async tx => {
|
||||
const order = await tx.order.update({
|
||||
where: { id: orderId, posAccountId },
|
||||
data: { status: OrderStatus.CANCELED },
|
||||
})
|
||||
await this.posWorkflow.onCancelOrder(tx, orderId)
|
||||
return order
|
||||
})
|
||||
return ResponseMapper.single(item)
|
||||
}
|
||||
|
||||
async getOrders(posId: number, status?: OrderStatus) {
|
||||
const where: Prisma.OrderWhereInput = {
|
||||
posAccountId: posId,
|
||||
status,
|
||||
}
|
||||
if (status) {
|
||||
where.status = status
|
||||
}
|
||||
const items = await this.prisma.order.findMany({
|
||||
where,
|
||||
include: {
|
||||
customer: {
|
||||
select: {
|
||||
id: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
},
|
||||
},
|
||||
orderItems: {
|
||||
include: {
|
||||
product: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
})
|
||||
return ResponseMapper.list(items)
|
||||
}
|
||||
|
||||
async getOrderDetails(posAccountId: number, orderId: number) {
|
||||
const item = await this.prisma.order.findUniqueOrThrow({
|
||||
where: { id: orderId, posAccountId },
|
||||
include: {
|
||||
customer: {
|
||||
select: {
|
||||
id: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
},
|
||||
},
|
||||
orderItems: {
|
||||
include: {
|
||||
product: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
return ResponseMapper.single(item)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import { Injectable } from '@nestjs/common'
|
||||
import { Prisma } from '../../../generated/prisma/client'
|
||||
import { SalesInvoicesWorkflow } from '../../sales-invoices/sales-invoices.workflow'
|
||||
|
||||
@Injectable()
|
||||
export class PosOrdersWorkflow {
|
||||
constructor(private salesInvoicesWorkflow: SalesInvoicesWorkflow) {}
|
||||
|
||||
async onCreateOrder(tx: Prisma.TransactionClient, orderId: number, posId: number) {
|
||||
const { inventoryId } = await tx.posAccount.findUniqueOrThrow({
|
||||
where: { id: posId },
|
||||
select: { inventoryId: true },
|
||||
})
|
||||
|
||||
const orderItems = await tx.orderItem.findMany({
|
||||
where: { orderId },
|
||||
select: {
|
||||
product: true,
|
||||
quantity: true,
|
||||
},
|
||||
})
|
||||
|
||||
await tx.stockReservation.createMany({
|
||||
data: orderItems.map(item => ({
|
||||
orderId,
|
||||
productId: item.product.id,
|
||||
quantity: item.quantity,
|
||||
inventoryId,
|
||||
})),
|
||||
})
|
||||
}
|
||||
|
||||
async onRejectOrder(tx: Prisma.TransactionClient, orderId: number) {
|
||||
const orderItemIds = await tx.orderItem
|
||||
.findMany({
|
||||
where: { orderId },
|
||||
select: {
|
||||
productId: true,
|
||||
},
|
||||
})
|
||||
.then(items => items.map(i => i.productId))
|
||||
|
||||
await tx.stockReservation.deleteMany({
|
||||
where: {
|
||||
orderId,
|
||||
productId: { in: orderItemIds },
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async onConfirmOrder(tx: Prisma.TransactionClient, orderId: number) {
|
||||
const orderItems = await tx.orderItem.findMany({
|
||||
where: { orderId },
|
||||
select: {
|
||||
product: true,
|
||||
quantity: true,
|
||||
},
|
||||
})
|
||||
|
||||
await tx.stockReservation.deleteMany({
|
||||
where: {
|
||||
orderId,
|
||||
productId: { in: orderItems.map(i => i.product.id) },
|
||||
},
|
||||
})
|
||||
|
||||
await this.salesInvoicesWorkflow.onCreateByOrder(tx, orderId)
|
||||
}
|
||||
|
||||
async onCancelOrder(tx: Prisma.TransactionClient, orderId: number) {
|
||||
const orderItemIds = await tx.orderItem
|
||||
.findMany({
|
||||
where: { orderId },
|
||||
select: {
|
||||
productId: true,
|
||||
},
|
||||
})
|
||||
.then(items => items.map(i => i.productId))
|
||||
|
||||
await tx.stockReservation.deleteMany({
|
||||
where: {
|
||||
orderId,
|
||||
productId: { in: orderItemIds },
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common'
|
||||
import { CreateSaleInvoiceDto } from './dto/create-pos.dto'
|
||||
import { Controller, Get, Param, Query } from '@nestjs/common'
|
||||
import { PosService } from './pos.service'
|
||||
|
||||
@Controller('pos')
|
||||
@@ -32,9 +31,4 @@ export class PosController {
|
||||
async getProductCategories(@Param('posId') posId: string) {
|
||||
return this.posService.getProductCategories(Number(posId))
|
||||
}
|
||||
|
||||
@Post('/:posId/orders/create')
|
||||
async createOrder(@Param('posId') posId: string, @Body() dto: CreateSaleInvoiceDto) {
|
||||
return this.posService.createOrder(Number(posId), dto)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Module } from '@nestjs/common'
|
||||
import { PrismaModule } from '../../prisma/prisma.module'
|
||||
import { PosOrdersModule } from './orders/orders.module'
|
||||
import { PosController } from './pos.controller'
|
||||
import { PosService } from './pos.service'
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
imports: [PrismaModule, PosOrdersModule],
|
||||
controllers: [PosController],
|
||||
providers: [PosService],
|
||||
})
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common'
|
||||
import { ResponseMapper } from '../../common/response/response-mapper'
|
||||
import { Prisma } from '../../generated/prisma/client'
|
||||
import { SalesInvoiceCreateInput } from '../../generated/prisma/models'
|
||||
import { PrismaService } from '../../prisma/prisma.service'
|
||||
import { CreateSaleInvoiceDto } from './dto/create-pos.dto'
|
||||
|
||||
@Injectable()
|
||||
export class PosService {
|
||||
@@ -53,67 +50,64 @@ export class PosService {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
q?: string
|
||||
productIds?: number[]
|
||||
} = {},
|
||||
) {
|
||||
const { isAvailable, page = 1, pageSize = 50, q } = options
|
||||
const { isAvailable, page = 1, pageSize = 50, q, productIds } = options
|
||||
const pos = await this.prisma.posAccount.findUniqueOrThrow({
|
||||
where: { id: posId },
|
||||
select: { inventoryId: true },
|
||||
})
|
||||
const inventoryId = pos.inventoryId
|
||||
|
||||
const query: Prisma.StockBalanceFindManyArgs = {
|
||||
where: {
|
||||
inventoryId,
|
||||
quantity:
|
||||
isAvailable === true
|
||||
? { gt: 0 }
|
||||
: isAvailable === false
|
||||
? { lte: 0 }
|
||||
: undefined,
|
||||
...(q && {
|
||||
product: {
|
||||
OR: [{ name: { contains: q } }, { sku: { contains: q } }],
|
||||
},
|
||||
}),
|
||||
},
|
||||
orderBy: {
|
||||
quantity: 'desc',
|
||||
},
|
||||
let sql = `
|
||||
SELECT sb.productId, sb.quantity - COALESCE(SUM(sr.quantity), 0) as availableQuantity, p.id, p.name, p.sku, p.description, p.salePrice, p.categoryId
|
||||
FROM Stock_Balance sb
|
||||
LEFT JOIN Stock_Reservations sr ON sb.productId = sr.productId AND sb.inventoryId = sr.inventoryId
|
||||
LEFT JOIN Products p ON sb.productId = p.id
|
||||
WHERE sb.inventoryId = ?
|
||||
`
|
||||
const params: any[] = [inventoryId]
|
||||
|
||||
if (productIds && productIds.length > 0) {
|
||||
sql += ` AND sb.productId IN (${productIds.map(() => '?').join(',')})`
|
||||
params.push(...productIds)
|
||||
}
|
||||
|
||||
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 }),
|
||||
])
|
||||
if (q) {
|
||||
sql += ` AND (p.name LIKE ? OR p.sku LIKE ?)`
|
||||
params.push(`%${q}%`, `%${q}%`)
|
||||
}
|
||||
|
||||
const mapped = items.map(item => ({
|
||||
id: item.id,
|
||||
quantity: Number(item.quantity),
|
||||
product: item.product,
|
||||
sql += ` GROUP BY sb.productId`
|
||||
|
||||
if (isAvailable !== undefined) {
|
||||
sql += isAvailable
|
||||
? ` HAVING availableQuantity > 0`
|
||||
: ` HAVING availableQuantity <= 0`
|
||||
}
|
||||
|
||||
sql += ` ORDER BY sb.quantity DESC`
|
||||
|
||||
const items = await this.prisma.$queryRawUnsafe(sql, ...params)
|
||||
|
||||
const mapped = (items as any[]).map(item => ({
|
||||
id: item.productId,
|
||||
quantity: Number(item.availableQuantity),
|
||||
product: {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
sku: item.sku,
|
||||
description: item.description,
|
||||
salePrice: item.salePrice,
|
||||
categoryId: item.categoryId,
|
||||
},
|
||||
}))
|
||||
|
||||
return ResponseMapper.paginate(mapped, count, page, pageSize)
|
||||
const count = mapped.length
|
||||
const paginated = mapped.slice((page - 1) * pageSize, page * pageSize)
|
||||
|
||||
return ResponseMapper.paginate(paginated, count, page, pageSize)
|
||||
}
|
||||
|
||||
async getProductCategories(posId: number) {
|
||||
@@ -157,41 +151,4 @@ export class PosService {
|
||||
const categories = Array.from(map.values())
|
||||
return ResponseMapper.list(categories)
|
||||
}
|
||||
|
||||
async createOrder(posId: number, data: CreateSaleInvoiceDto) {
|
||||
const { customerId, items, ...res } = data
|
||||
const lastCode = await this.prisma.salesInvoice
|
||||
.findFirst({
|
||||
where: { posAccountId: posId },
|
||||
orderBy: { code: 'desc' },
|
||||
select: { code: true },
|
||||
})
|
||||
.then(si => si?.code || '0')
|
||||
|
||||
const newCode = String(Number(lastCode) + 1)
|
||||
|
||||
const totalAmount = data.items.reduce(
|
||||
(acc, item) => acc + item.count * item.unitPrice,
|
||||
0,
|
||||
)
|
||||
|
||||
const preparedOrder: SalesInvoiceCreateInput = {
|
||||
...res,
|
||||
code: newCode,
|
||||
posAccount: { connect: { id: posId } },
|
||||
customer: customerId ? { connect: { id: customerId } } : undefined,
|
||||
totalAmount: totalAmount,
|
||||
items: {
|
||||
create: items.map(item => ({
|
||||
product: { connect: { id: Number(item.productId) } },
|
||||
count: item.count,
|
||||
unitPrice: item.unitPrice,
|
||||
total: item.count * item.unitPrice,
|
||||
})),
|
||||
},
|
||||
}
|
||||
|
||||
const item = await this.prisma.salesInvoice.create({ data: preparedOrder })
|
||||
return ResponseMapper.create(item)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user