inventory
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty, IsArray, ValidateNested, ArrayMinSize, IsString, IsNumber, Min, IsDate } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
export class BulkReserveFoodItemDto {
|
||||
@ApiProperty({ example: 'food-123', description: 'Food ID' })
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
foodId!: string;
|
||||
|
||||
@ApiProperty({ example: 5, description: 'Quantity to reserve' })
|
||||
@IsNotEmpty()
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
@Type(() => Number)
|
||||
quantity!: number;
|
||||
|
||||
@ApiProperty({ example: '2024-12-31T23:59:59Z', description: 'Reservation expiration date' })
|
||||
@IsNotEmpty()
|
||||
@IsDate()
|
||||
@Type(() => Date)
|
||||
expiresAt!: Date;
|
||||
}
|
||||
|
||||
export class BulkReserveFoodDto {
|
||||
@ApiProperty({
|
||||
description: 'Array of food reservations to create',
|
||||
type: [BulkReserveFoodItemDto],
|
||||
example: [
|
||||
{ foodId: 'food-123', quantity: 5, expiresAt: '2024-12-31T23:59:59Z' },
|
||||
{ foodId: 'food-789', quantity: 3, expiresAt: '2024-12-31T23:59:59Z' },
|
||||
],
|
||||
})
|
||||
@IsNotEmpty()
|
||||
@IsArray()
|
||||
@ArrayMinSize(1, { message: 'At least one item is required' })
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => BulkReserveFoodItemDto)
|
||||
items!: BulkReserveFoodItemDto[];
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty, IsArray, ValidateNested, ArrayMinSize } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import { SetStockDto } from './set-stock.dto';
|
||||
|
||||
export class BulkSetStockItemDto extends SetStockDto {
|
||||
@ApiProperty({ example: 'food-123', description: 'Food ID' })
|
||||
@IsNotEmpty()
|
||||
foodId!: string;
|
||||
}
|
||||
|
||||
export class BulkSetStockDto {
|
||||
@ApiProperty({
|
||||
description: 'Array of stock items to set',
|
||||
type: [BulkSetStockItemDto],
|
||||
example: [
|
||||
{ foodId: 'food-123', totalStock: 100, availableStock: 80 },
|
||||
{ foodId: 'food-456', totalStock: 50, availableStock: 45 },
|
||||
],
|
||||
})
|
||||
@IsNotEmpty()
|
||||
@IsArray()
|
||||
@ArrayMinSize(1, { message: 'At least one item is required' })
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => BulkSetStockItemDto)
|
||||
items!: BulkSetStockItemDto[];
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export class CreateInventoryDto {}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNumber, Min } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
export class SetStockDto {
|
||||
@ApiProperty({ example: 100, description: 'Total stock quantity' })
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
@Type(() => Number)
|
||||
totalStock!: number;
|
||||
|
||||
@ApiProperty({ example: 80, description: 'Available stock quantity' })
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
@Type(() => Number)
|
||||
availableStock!: number;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { PartialType } from '@nestjs/swagger';
|
||||
import { CreateInventoryDto } from './create-inventory.dto';
|
||||
|
||||
export class UpdateInventoryDto extends PartialType(CreateInventoryDto) {}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Entity, ManyToOne, Property, Unique } from '@mikro-orm/core';
|
||||
import { BaseEntity } from '../../../common/entities/base.entity';
|
||||
import { Restaurant } from '../../restaurants/entities/restaurant.entity';
|
||||
import { Food } from '../../foods/entities/food.entity';
|
||||
|
||||
@Entity({ tableName: 'inventory' })
|
||||
@Unique({ properties: ['food', 'restaurant'] })
|
||||
export class Inventory extends BaseEntity {
|
||||
@ManyToOne(() => Food, { unique: true })
|
||||
food!: Food;
|
||||
|
||||
@ManyToOne(() => Restaurant)
|
||||
restaurant!: Restaurant;
|
||||
|
||||
@Property({ type: 'int' })
|
||||
totalStock!: number;
|
||||
|
||||
@Property({ type: 'int' })
|
||||
availableStock!: number;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Entity, Enum, ManyToOne, Property } from '@mikro-orm/core';
|
||||
import { BaseEntity } from '../../../common/entities/base.entity';
|
||||
import { Food } from '../../foods/entities/food.entity';
|
||||
import { Order } from '../../orders/entities/order.entity';
|
||||
import { ReservationStatus } from '../inteface/reservation';
|
||||
|
||||
@Entity({ tableName: 'reservations' })
|
||||
export class Reservation extends BaseEntity {
|
||||
@ManyToOne(() => Food)
|
||||
food!: Food;
|
||||
|
||||
@ManyToOne(() => Order)
|
||||
order!: Order;
|
||||
|
||||
@Property({ type: 'int' })
|
||||
quantity!: number;
|
||||
|
||||
@Property({ type: 'date' })
|
||||
expiresAt!: Date;
|
||||
|
||||
@Enum(() => ReservationStatus)
|
||||
status: ReservationStatus = ReservationStatus.ACTIVE;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export enum ReservationStatus {
|
||||
ACTIVE = 'active',
|
||||
CONFIRMED = 'confirmed',
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Controller, Body, Patch, Param, Post, UseGuards } from '@nestjs/common';
|
||||
import { InventoryService } from './inventory.service';
|
||||
import { SetStockDto } from './dto/set-stock.dto';
|
||||
import { BulkSetStockDto } from './dto/bulk-set-stock.dto';
|
||||
import { ApiTags, ApiOperation, ApiBody, ApiParam, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { AdminAuthGuard } from 'src/modules/auth/guards/adminAuth.guard';
|
||||
import { RestId } from 'src/common/decorators';
|
||||
import { Inventory } from './entities/inventory.entity';
|
||||
|
||||
@ApiTags('inventory')
|
||||
@Controller()
|
||||
export class InventoryController {
|
||||
constructor(private readonly inventoryService: InventoryService) {}
|
||||
|
||||
@Patch('admin/inventory/food/:foodId/stock')
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Set available and total stock for a food item' })
|
||||
@ApiParam({ name: 'foodId', description: 'Food ID' })
|
||||
@ApiBody({ type: SetStockDto })
|
||||
setStockForFood(
|
||||
@Param('foodId') foodId: string,
|
||||
@RestId() restaurantId: string,
|
||||
@Body() setStockDto: SetStockDto,
|
||||
): Promise<Inventory> {
|
||||
return this.inventoryService.setStockForFood(foodId, restaurantId, setStockDto);
|
||||
}
|
||||
|
||||
@Post('admin/inventory/foods/stock/bulk')
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Bulk set available and total stock for multiple food items' })
|
||||
@ApiBody({ type: BulkSetStockDto })
|
||||
bulkSetStockForFoods(@RestId() restaurantId: string, @Body() bulkSetStockDto: BulkSetStockDto): Promise<Inventory[]> {
|
||||
return this.inventoryService.bulkSetStockForFoods(restaurantId, bulkSetStockDto);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { MikroOrmModule } from '@mikro-orm/nestjs';
|
||||
import { InventoryService } from './inventory.service';
|
||||
import { InventoryController } from './inventory.controller';
|
||||
import { Inventory } from './entities/inventory.entity';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
|
||||
@Module({
|
||||
imports: [MikroOrmModule.forFeature([Inventory]), AuthModule],
|
||||
controllers: [InventoryController],
|
||||
providers: [InventoryService],
|
||||
})
|
||||
export class InventoryModule {}
|
||||
@@ -0,0 +1,221 @@
|
||||
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
|
||||
import { EntityManager } from '@mikro-orm/postgresql';
|
||||
import { SetStockDto } from './dto/set-stock.dto';
|
||||
import { BulkSetStockDto } from './dto/bulk-set-stock.dto';
|
||||
import { BulkReserveFoodDto } from './dto/bulk-reserve-food.dto';
|
||||
import { Inventory } from './entities/inventory.entity';
|
||||
import { Reservation } from './entities/reservation.entity';
|
||||
import { Food } from '../foods/entities/food.entity';
|
||||
import { Restaurant } from '../restaurants/entities/restaurant.entity';
|
||||
import { Order } from '../orders/entities/order.entity';
|
||||
|
||||
@Injectable()
|
||||
export class InventoryService {
|
||||
constructor(private readonly em: EntityManager) {}
|
||||
|
||||
async setStockForFood(foodId: string, restaurantId: string, setStockDto: SetStockDto): Promise<Inventory> {
|
||||
// Validate that availableStock doesn't exceed totalStock
|
||||
if (setStockDto.availableStock > setStockDto.totalStock) {
|
||||
throw new BadRequestException('Available stock cannot exceed total stock');
|
||||
}
|
||||
|
||||
// Find food and verify it belongs to the restaurant
|
||||
const food = await this.em.findOne(Food, { id: foodId }, { populate: ['restaurant'] });
|
||||
if (!food) {
|
||||
throw new NotFoundException(`Food with ID ${foodId} not found`);
|
||||
}
|
||||
|
||||
if (food.restaurant.id !== restaurantId) {
|
||||
throw new BadRequestException(`Food does not belong to restaurant ${restaurantId}`);
|
||||
}
|
||||
|
||||
// Find or create inventory record
|
||||
let inventory = await this.em.findOne(Inventory, {
|
||||
food: { id: foodId },
|
||||
restaurant: { id: restaurantId },
|
||||
});
|
||||
|
||||
if (!inventory) {
|
||||
// Create new inventory record
|
||||
const restaurant = await this.em.findOne(Restaurant, { id: restaurantId });
|
||||
if (!restaurant) {
|
||||
throw new NotFoundException(`Restaurant with ID ${restaurantId} not found`);
|
||||
}
|
||||
|
||||
inventory = this.em.create(Inventory, {
|
||||
food,
|
||||
restaurant,
|
||||
totalStock: setStockDto.totalStock,
|
||||
availableStock: setStockDto.availableStock,
|
||||
});
|
||||
} else {
|
||||
// Update existing inventory record
|
||||
inventory.totalStock = setStockDto.totalStock;
|
||||
inventory.availableStock = setStockDto.availableStock;
|
||||
}
|
||||
|
||||
await this.em.flush();
|
||||
|
||||
return inventory;
|
||||
}
|
||||
|
||||
async bulkSetStockForFoods(restaurantId: string, bulkSetStockDto: BulkSetStockDto): Promise<Inventory[]> {
|
||||
const { items } = bulkSetStockDto;
|
||||
|
||||
// Validate all items first
|
||||
for (const item of items) {
|
||||
if (item.availableStock > item.totalStock) {
|
||||
throw new BadRequestException(`Available stock cannot exceed total stock for food ${item.foodId}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Get all food IDs
|
||||
const foodIds = items.map(item => item.foodId);
|
||||
|
||||
// Load all foods in one query
|
||||
const foods = await this.em.find(Food, { id: { $in: foodIds } }, { populate: ['restaurant'] });
|
||||
|
||||
// Verify all foods exist and belong to the restaurant
|
||||
const foodMap = new Map<string, Food>();
|
||||
for (const food of foods) {
|
||||
if (food.restaurant.id !== restaurantId) {
|
||||
throw new BadRequestException(`Food ${food.id} does not belong to restaurant ${restaurantId}`);
|
||||
}
|
||||
foodMap.set(food.id, food);
|
||||
}
|
||||
|
||||
// Check for missing foods
|
||||
const missingFoodIds = foodIds.filter(id => !foodMap.has(id));
|
||||
if (missingFoodIds.length > 0) {
|
||||
throw new NotFoundException(`Foods not found: ${missingFoodIds.join(', ')}`);
|
||||
}
|
||||
|
||||
// Load all existing inventories in one query
|
||||
const existingInventories = await this.em.find(Inventory, {
|
||||
food: { id: { $in: foodIds } },
|
||||
restaurant: { id: restaurantId },
|
||||
});
|
||||
|
||||
const inventoryMap = new Map<string, Inventory>();
|
||||
for (const inventory of existingInventories) {
|
||||
inventoryMap.set(inventory.food.id, inventory);
|
||||
}
|
||||
|
||||
// Get restaurant once
|
||||
const restaurant = await this.em.findOne(Restaurant, { id: restaurantId });
|
||||
if (!restaurant) {
|
||||
throw new NotFoundException(`Restaurant with ID ${restaurantId} not found`);
|
||||
}
|
||||
|
||||
// Process all items
|
||||
const results: Inventory[] = [];
|
||||
for (const item of items) {
|
||||
const food = foodMap.get(item.foodId)!;
|
||||
let inventory = inventoryMap.get(item.foodId);
|
||||
|
||||
if (!inventory) {
|
||||
// Create new inventory record
|
||||
inventory = this.em.create(Inventory, {
|
||||
food,
|
||||
restaurant,
|
||||
totalStock: item.totalStock,
|
||||
availableStock: item.availableStock,
|
||||
});
|
||||
} else {
|
||||
// Update existing inventory record
|
||||
inventory.totalStock = item.totalStock;
|
||||
inventory.availableStock = item.availableStock;
|
||||
}
|
||||
|
||||
results.push(inventory);
|
||||
}
|
||||
|
||||
// Flush all changes at once
|
||||
await this.em.flush();
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
async bulkReserveFood(
|
||||
restaurantId: string,
|
||||
orderId: string,
|
||||
bulkReserveFoodDto: BulkReserveFoodDto,
|
||||
): Promise<Reservation[]> {
|
||||
const { items } = bulkReserveFoodDto;
|
||||
|
||||
// Get all unique food IDs
|
||||
const foodIds = [...new Set(items.map(item => item.foodId))];
|
||||
|
||||
// Load all foods in one query
|
||||
const foods = await this.em.find(Food, { id: { $in: foodIds } }, { populate: ['restaurant'] });
|
||||
|
||||
// Verify all foods exist and belong to the restaurant
|
||||
const foodMap = new Map<string, Food>();
|
||||
for (const food of foods) {
|
||||
if (food.restaurant.id !== restaurantId) {
|
||||
throw new BadRequestException(`Food ${food.id} does not belong to restaurant ${restaurantId}`);
|
||||
}
|
||||
foodMap.set(food.id, food);
|
||||
}
|
||||
|
||||
// Check for missing foods
|
||||
const missingFoodIds = foodIds.filter(id => !foodMap.has(id));
|
||||
if (missingFoodIds.length > 0) {
|
||||
throw new NotFoundException(`Foods not found: ${missingFoodIds.join(', ')}`);
|
||||
}
|
||||
|
||||
// Load order
|
||||
const order = await this.em.findOne(Order, { id: orderId });
|
||||
if (!order) {
|
||||
throw new NotFoundException(`Order with ID ${orderId} not found`);
|
||||
}
|
||||
|
||||
// Load all existing inventories in one query
|
||||
const existingInventories = await this.em.find(Inventory, {
|
||||
food: { id: { $in: foodIds } },
|
||||
restaurant: { id: restaurantId },
|
||||
});
|
||||
|
||||
const inventoryMap = new Map<string, Inventory>();
|
||||
for (const inventory of existingInventories) {
|
||||
inventoryMap.set(inventory.food.id, inventory);
|
||||
}
|
||||
|
||||
// Validate stock availability and create reservations
|
||||
const reservations: Reservation[] = [];
|
||||
for (const item of items) {
|
||||
const food = foodMap.get(item.foodId)!;
|
||||
const inventory = inventoryMap.get(item.foodId);
|
||||
|
||||
// Check if inventory exists
|
||||
if (!inventory) {
|
||||
throw new NotFoundException(`Inventory not found for food ${item.foodId}`);
|
||||
}
|
||||
|
||||
// Check if available stock is sufficient
|
||||
if (inventory.availableStock < item.quantity) {
|
||||
throw new BadRequestException(
|
||||
`Insufficient stock for food ${item.foodId}. Available: ${inventory.availableStock}, Requested: ${item.quantity}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Create reservation record
|
||||
const reservation = this.em.create(Reservation, {
|
||||
food,
|
||||
order,
|
||||
quantity: item.quantity,
|
||||
expiresAt: item.expiresAt,
|
||||
});
|
||||
|
||||
reservations.push(reservation);
|
||||
|
||||
// Update available stock (decrease by quantity)
|
||||
inventory.availableStock -= item.quantity;
|
||||
}
|
||||
|
||||
// Flush all changes at once
|
||||
await this.em.flush();
|
||||
|
||||
return reservations;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user