add stock to variant

This commit is contained in:
2026-02-14 15:41:44 +03:30
parent ec828717b5
commit 942094118c
11 changed files with 52 additions and 292 deletions
@@ -57,12 +57,18 @@ export class CartItemService {
}
/**
* Add or increment item in cart
* Add or increment item in cart (validates stock against existing + new quantity)
*/
async addOrIncrementItem(cart: Cart, variantId: string, shopId: string, quantityToAdd: number): Promise<void> {
const variant = await this.validationService.validateAndGetVariant(variantId, shopId, quantityToAdd);
const index = this.getItemIndex(cart, variantId);
const existingQuantityInCart = index >= 0 ? cart.items[index].quantity : 0;
const index = this.getItemIndex(cart, variant.id);
const variant = await this.validationService.validateAndGetVariant(
variantId,
shopId,
quantityToAdd,
existingQuantityInCart,
);
if (index < 0) {
cart.items.push(this.createCartItem(variant, quantityToAdd));
@@ -29,15 +29,28 @@ export class CartValidationService {
) { }
/**
* Validate product exists and belongs to shop
* Validate variant exists, belongs to shop, and has sufficient stock.
* @param existingQuantityInCart - current quantity of this variant already in cart (for add/increment)
*/
async validateAndGetVariant(variantId: string, shopId: string, quantity: number): Promise<Variant> {
const variant = await this.productService.findOrFailVariant(variantId)
async validateAndGetVariant(
variantId: string,
shopId: string,
quantityToAdd: number,
existingQuantityInCart: number = 0,
): Promise<Variant> {
const variant = await this.productService.findOrFailVariant(variantId);
if (variant.product.shop.id !== shopId) {
throw new BadRequestException(CartMessage.PRODUCT_NOT_BELONGS_TO_SHOP);
}
if (variant.stock != null) {
const totalRequested = existingQuantityInCart + quantityToAdd;
if (totalRequested > variant.stock) {
throw new BadRequestException(CartMessage.INSUFFICIENT_STOCK);
}
}
return variant;
}
@@ -20,7 +20,7 @@ import { Payment } from 'src/modules/payments/entities/payment.entity';
import { StatusTransitionRef } from '../interface/order.interface';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { OrderCreatedEvent, OrderStatusChangedEvent } from '../events/order.events';
import { OrderMessage } from 'src/common/enums/message.enum';
import { CartMessage, OrderMessage } from 'src/common/enums/message.enum';
import { UserService } from 'src/modules/users/providers/user.service';
import { ShopService } from 'src/modules/shops/providers/shops.service';
import { DeliveryService } from 'src/modules/delivery/providers/delivery.service';
@@ -370,6 +370,10 @@ export class OrdersService {
throw new BadRequestException(OrderMessage.PRODUCT_NOT_BELONGS_TO_SHOP);
}
if (variant.stock != null && cartItem.quantity > variant.stock) {
throw new BadRequestException(CartMessage.INSUFFICIENT_STOCK);
}
orderItemsData.push({
variant,
quantity: cartItem.quantity,
@@ -353,7 +353,7 @@ export class PaymentsService {
if (shopId) {
params.push(shopId);
restaurantFilter = `AND o.restaurant_id = ?`;
restaurantFilter = `AND o.shop_id = ?`;
}
// MikroORM uses ? placeholders for parameter binding
@@ -31,8 +31,14 @@ export class CreateVariantDto {
@Min(1000)
@IsNotEmpty()
@ApiProperty()
price: number
price: number;
@IsOptional()
@IsInt()
@Min(0)
@Type(() => Number)
@ApiPropertyOptional({ description: 'Available stock; omit or null for unlimited', example: 10 })
stock?: number | null;
}
export class CreateProductDto {
@@ -61,6 +67,7 @@ export class CreateProductDto {
id: '',
value: 'قرمز',
price: 100000,
stock: 10,
}]
})
variants: CreateVariantDto[]
@@ -14,4 +14,7 @@ export class Variant extends BaseEntity {
@Property({ type: 'decimal', precision: 10, scale: 0, nullable: true })
price?: number;
/** Available quantity; null means unlimited */
@Property({ type: 'int', nullable: true, default: null })
stock: number | null = null;
}
@@ -59,9 +59,10 @@ export class ProductService {
product,
value: variant.value,
price: variant.price,
})
stock: variant.stock ?? null,
});
em.persist(variantRecord)
em.persist(variantRecord);
}
await em.flush();
@@ -136,7 +137,7 @@ export class ProductService {
const updatedVariantIds = new Set<string>();
for (const variant of variants) {
const { id, value, price } = variant;
const { id, value, price, stock } = variant;
if (id) {
// Update existing variant
@@ -146,7 +147,7 @@ export class ProductService {
}
// Update the variant fields
this.em.assign(existingVariant, { value, price });
this.em.assign(existingVariant, { value, price, stock: stock ?? null });
updatedVariantIds.add(id);
} else {
// Create new variant
@@ -154,6 +155,7 @@ export class ProductService {
product,
value,
price,
stock: stock ?? null,
});
this.em.persist(variantRecord);
}
+1 -1
View File
@@ -10,7 +10,7 @@ export interface ProductData {
discount?: number;
score?: number;
isSpecialOffer?: boolean;
varrinats: { price: number, value: string | null }[]
varrinats: { price: number; value: string | null; stock?: number | null }[]
attribute?: null | string
}
+2 -2
View File
@@ -23,7 +23,7 @@ export const shopsData: ShopData[] = [
{
name: 'ژیوان',
slug: 'zhivan',
domain: 'https://dkala-front.dev.danakcorp.com/zhivan',
domain: 'https://dkala.danakcorp.com/zhivan',
isActive: true,
phone: '',
logo: 'https://dmenu.danakcorp.com/uploads/images/store/store_1638870352717_61a3661f37a0d33354a6d210.png',
@@ -42,7 +42,7 @@ export const shopsData: ShopData[] = [
{
name: 'بوته',
slug: 'boote',
domain: 'https://dkala-front.dev.danakcorp.com/boote',
domain: 'https://dkala.danakcorp.com/boote',
isActive: true,
phone: '',
logo: 'https://dmenu.danakcorp.com/uploads/images/store/store_1641199914860_61d2b72b2e8bd97126a70b5f.png',
+1
View File
@@ -62,6 +62,7 @@ export class ProductsSeeder {
product,
value: variant.value ?? '',
price: variant.price,
stock: variant.stock ?? null,
});
em.persist(variantRecord);
});