update order
This commit is contained in:
@@ -0,0 +1,21 @@
|
|||||||
|
import { Migration } from '@mikro-orm/migrations';
|
||||||
|
|
||||||
|
export class Migration20260628120000_DecimalQuantities extends Migration {
|
||||||
|
|
||||||
|
override async up(): Promise<void> {
|
||||||
|
this.addSql(`alter table "order_items" alter column "quantity" type numeric(10,3) using "quantity"::numeric(10,3);`);
|
||||||
|
this.addSql(`alter table "orders" alter column "total_items" type numeric(10,3) using "total_items"::numeric(10,3);`);
|
||||||
|
this.addSql(`alter table "products" alter column "min_purchase_quantity" type numeric(10,3) using "min_purchase_quantity"::numeric(10,3);`);
|
||||||
|
this.addSql(`alter table "products" alter column "max_purchase_quantity" type numeric(10,3) using "max_purchase_quantity"::numeric(10,3);`);
|
||||||
|
this.addSql(`alter table "variants" alter column "stock" type numeric(10,3) using "stock"::numeric(10,3);`);
|
||||||
|
}
|
||||||
|
|
||||||
|
override async down(): Promise<void> {
|
||||||
|
this.addSql(`alter table "order_items" alter column "quantity" type int using round("quantity")::int;`);
|
||||||
|
this.addSql(`alter table "orders" alter column "total_items" type int using round("total_items")::int;`);
|
||||||
|
this.addSql(`alter table "products" alter column "min_purchase_quantity" type int using round("min_purchase_quantity")::int;`);
|
||||||
|
this.addSql(`alter table "products" alter column "max_purchase_quantity" type int using round("max_purchase_quantity")::int;`);
|
||||||
|
this.addSql(`alter table "variants" alter column "stock" type int using round("stock")::int;`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -761,6 +761,8 @@ export const enum CartMessage {
|
|||||||
ITEM_NOT_FOUND = 'آیتم در سبد خرید یافت نشد',
|
ITEM_NOT_FOUND = 'آیتم در سبد خرید یافت نشد',
|
||||||
DELIVERY_METHOD_NOT_FOUND_FOR_SHOP = 'روش ارسال برای این فروشگاه یافت نشد',
|
DELIVERY_METHOD_NOT_FOUND_FOR_SHOP = 'روش ارسال برای این فروشگاه یافت نشد',
|
||||||
COUPON_CANNOT_BE_APPLIED = 'این کوپن قابل اعمال بر روی آیتمهای سبد خرید شما نیست. لطفا آیتمهایی از دستهبندیها یا غذاهای مشخص شده اضافه کنید',
|
COUPON_CANNOT_BE_APPLIED = 'این کوپن قابل اعمال بر روی آیتمهای سبد خرید شما نیست. لطفا آیتمهایی از دستهبندیها یا غذاهای مشخص شده اضافه کنید',
|
||||||
|
MIN_PURCHASE_NOT_MET = 'مقدار درخواستی کمتر از حداقل مجاز خرید این محصول است',
|
||||||
|
QUANTITY_STEP_MISMATCH = 'مقدار باید مضربی از گام خرید محصول باشد',
|
||||||
PRODUCTS_MUST_HAVE_PICKUP_SERVE_FOR_COURIER = 'تمام محصولات باید قابلیت تحویل پیک داشته باشند. محصولات زیر این قابلیت را ندارند: [productTitles]',
|
PRODUCTS_MUST_HAVE_PICKUP_SERVE_FOR_COURIER = 'تمام محصولات باید قابلیت تحویل پیک داشته باشند. محصولات زیر این قابلیت را ندارند: [productTitles]',
|
||||||
PRODUCT_ONLY_AVAILABLE_DURING_MEAL_TIMES = 'محصول [productTitle] فقط در ساعات وعدههای غذایی (صبحانه، ناهار، عصرانه یا شام) در دسترس است. زمان فعلی خارج از ساعات وعدههای غذایی است',
|
PRODUCT_ONLY_AVAILABLE_DURING_MEAL_TIMES = 'محصول [productTitle] فقط در ساعات وعدههای غذایی (صبحانه، ناهار، عصرانه یا شام) در دسترس است. زمان فعلی خارج از ساعات وعدههای غذایی است',
|
||||||
PRODUCT_ONLY_AVAILABLE_FOR_MEAL_TYPES = 'محصول [productTitle] فقط برای [allowedMealTypes] در دسترس است. زمان فعلی [mealType] است که با این محصول سازگار نیست',
|
PRODUCT_ONLY_AVAILABLE_FOR_MEAL_TYPES = 'محصول [productTitle] فقط برای [allowedMealTypes] در دسترس است. زمان فعلی [mealType] است که با این محصول سازگار نیست',
|
||||||
|
|||||||
@@ -24,14 +24,14 @@ export class CartController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post('variant/:variantId/increment')
|
@Post('variant/:variantId/increment')
|
||||||
@ApiOperation({ summary: 'Increment item quantity in cart by 1' })
|
@ApiOperation({ summary: 'Increment item quantity in cart by product purchase step' })
|
||||||
@ApiHeader(API_HEADER_SLUG)
|
@ApiHeader(API_HEADER_SLUG)
|
||||||
async incrementItem(@UserId() userId: string, @ShopId() shopId: string, @Param('variantId') variantId: string) {
|
async incrementItem(@UserId() userId: string, @ShopId() shopId: string, @Param('variantId') variantId: string) {
|
||||||
return this.cartService.incrementItem(userId, shopId, variantId, 1);
|
return this.cartService.incrementItemByStep(userId, shopId, variantId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('variant/:variantId/decrement')
|
@Post('variant/:variantId/decrement')
|
||||||
@ApiOperation({ summary: 'Decrement item quantity in cart by 1 (removes item if quantity reaches 0)' })
|
@ApiOperation({ summary: 'Decrement item quantity by product purchase step (removes item if quantity reaches 0)' })
|
||||||
@ApiHeader(API_HEADER_SLUG)
|
@ApiHeader(API_HEADER_SLUG)
|
||||||
async decrementItem(@UserId() userId: string, @ShopId() shopId: string, @Param('variantId') variantId: string) {
|
async decrementItem(@UserId() userId: string, @ShopId() shopId: string, @Param('variantId') variantId: string) {
|
||||||
return this.cartService.decrementItem(userId, shopId, variantId);
|
return this.cartService.decrementItem(userId, shopId, variantId);
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { IsNotEmpty, IsArray, ValidateNested, ArrayMinSize, IsInt, Min, IsString } from 'class-validator';
|
import { IsNotEmpty, IsArray, ValidateNested, ArrayMinSize, IsNumber, Min, IsString } from 'class-validator';
|
||||||
import { Type } from 'class-transformer';
|
import { Type } from 'class-transformer';
|
||||||
import { CartMessage } from 'src/common/enums/message.enum';
|
import { CartMessage } from 'src/common/enums/message.enum';
|
||||||
|
|
||||||
class AddItemToCartDto {
|
class AddItemToCartDto {
|
||||||
@ApiProperty({ description: 'Quantity of the product item', example: 1, minimum: 1 })
|
@ApiProperty({ description: 'Quantity of the product item (supports decimals for variable products)', example: 1.5, minimum: 0.001 })
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@Type(() => Number)
|
@Type(() => Number)
|
||||||
@IsInt()
|
@IsNumber({ maxDecimalPlaces: 3 })
|
||||||
@Min(1)
|
@Min(0.001)
|
||||||
quantity!: number;
|
quantity!: number;
|
||||||
|
|
||||||
@ApiProperty({ description: 'Product ID' })
|
@ApiProperty({ description: 'Product ID' })
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { IsNotEmpty, IsString, IsArray, ValidateNested, IsInt, Min } from 'class-validator';
|
import { IsNotEmpty, IsString, IsArray, ValidateNested, IsNumber, Min } from 'class-validator';
|
||||||
import { Type } from 'class-transformer';
|
import { Type } from 'class-transformer';
|
||||||
|
|
||||||
export class CartItemDto {
|
export class CartItemDto {
|
||||||
@@ -8,11 +8,11 @@ export class CartItemDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
variantId!: string;
|
variantId!: string;
|
||||||
|
|
||||||
@ApiProperty({ description: 'Quantity of the product item', example: 2, minimum: 1 })
|
@ApiProperty({ description: 'Quantity of the product item (supports decimals for variable products)', example: 1.5, minimum: 0.001 })
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@Type(() => Number)
|
@Type(() => Number)
|
||||||
@IsInt()
|
@IsNumber({ maxDecimalPlaces: 3 })
|
||||||
@Min(1)
|
@Min(0.001)
|
||||||
quantity!: number;
|
quantity!: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { CartCalculationService } from './cart-calculation.service';
|
|||||||
import { CartMessage } from 'src/common/enums/message.enum';
|
import { CartMessage } from 'src/common/enums/message.enum';
|
||||||
import { ProductService } from 'src/modules/products/providers/product.service';
|
import { ProductService } from 'src/modules/products/providers/product.service';
|
||||||
import { Variant } from 'src/modules/products/entities/variant.entity';
|
import { Variant } from 'src/modules/products/entities/variant.entity';
|
||||||
|
import { getQuantityStep, roundQuantity } from 'src/modules/products/utils/quantity.utils';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CartItemService {
|
export class CartItemService {
|
||||||
@@ -22,16 +23,17 @@ export class CartItemService {
|
|||||||
const itemPrice = variant.price || 0;
|
const itemPrice = variant.price || 0;
|
||||||
const itemDiscount = variant.product.discount || 0;
|
const itemDiscount = variant.product.discount || 0;
|
||||||
const image = variant.product.images?.[0];
|
const image = variant.product.images?.[0];
|
||||||
|
const normalizedQuantity = roundQuantity(quantity);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
variantId: variant.id,
|
variantId: variant.id,
|
||||||
productTitle: variant.product.title,
|
productTitle: variant.product.title,
|
||||||
variantValue: variant.value,
|
variantValue: variant.value,
|
||||||
image,
|
image,
|
||||||
quantity,
|
quantity: normalizedQuantity,
|
||||||
price: itemPrice,
|
price: itemPrice,
|
||||||
discount: itemDiscount,
|
discount: itemDiscount,
|
||||||
totalPrice: this.calculationService.calculateItemTotalPrice(itemPrice, itemDiscount, quantity),
|
totalPrice: this.calculationService.calculateItemTotalPrice(itemPrice, itemDiscount, normalizedQuantity),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,16 +44,17 @@ export class CartItemService {
|
|||||||
const itemPrice = variant.price || 0;
|
const itemPrice = variant.price || 0;
|
||||||
const itemDiscount = variant.product.discount || 0;
|
const itemDiscount = variant.product.discount || 0;
|
||||||
const image = variant.product.images?.[0];
|
const image = variant.product.images?.[0];
|
||||||
|
const normalizedQuantity = roundQuantity(quantity);
|
||||||
return {
|
return {
|
||||||
...(previous ?? ({} as CartItem)),
|
...(previous ?? ({} as CartItem)),
|
||||||
variantId: variant.id,
|
variantId: variant.id,
|
||||||
productTitle: variant.product.title,
|
productTitle: variant.product.title,
|
||||||
variantValue: variant.value,
|
variantValue: variant.value,
|
||||||
image,
|
image,
|
||||||
quantity,
|
quantity: normalizedQuantity,
|
||||||
price: itemPrice,
|
price: itemPrice,
|
||||||
discount: itemDiscount,
|
discount: itemDiscount,
|
||||||
totalPrice: this.calculationService.calculateItemTotalPrice(itemPrice, itemDiscount, quantity),
|
totalPrice: this.calculationService.calculateItemTotalPrice(itemPrice, itemDiscount, normalizedQuantity),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,11 +86,20 @@ export class CartItemService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const existingItem = cart.items[index];
|
const existingItem = cart.items[index];
|
||||||
const newQuantity = existingItem.quantity + quantityToAdd;
|
const newQuantity = roundQuantity(existingItem.quantity + quantityToAdd);
|
||||||
|
|
||||||
cart.items[index] = this.buildCartItemFromFood(variant, newQuantity, existingItem);
|
cart.items[index] = this.buildCartItemFromFood(variant, newQuantity, existingItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increment item by product purchase step (1 for fixed, purchasePitch for variable)
|
||||||
|
*/
|
||||||
|
async incrementByStep(cart: Cart, variantId: string, shopId: string): Promise<void> {
|
||||||
|
const variant = await this.productService.findOrFailVariant(variantId);
|
||||||
|
const step = getQuantityStep(variant.product);
|
||||||
|
await this.addOrIncrementItem(cart, variantId, shopId, step);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove item from cart
|
* Remove item from cart
|
||||||
*/
|
*/
|
||||||
@@ -109,14 +121,15 @@ export class CartItemService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const existingItem = cart.items[itemIndex];
|
const existingItem = cart.items[itemIndex];
|
||||||
const newQuantity = existingItem.quantity - 1;
|
const variant = await this.productService.findOrFailVariant(variantId);
|
||||||
|
const step = getQuantityStep(variant.product);
|
||||||
|
const newQuantity = roundQuantity(existingItem.quantity - step);
|
||||||
|
|
||||||
if (newQuantity <= 0) {
|
if (newQuantity <= 0) {
|
||||||
cart.items.splice(itemIndex, 1);
|
cart.items.splice(itemIndex, 1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const variant = await this.productService.findOrFailVariant(variantId);
|
|
||||||
cart.items[itemIndex] = this.buildCartItemFromFood(variant, newQuantity, existingItem);
|
cart.items[itemIndex] = this.buildCartItemFromFood(variant, newQuantity, existingItem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { ShopService } from 'src/modules/shops/providers/shops.service';
|
|||||||
import { DeliveryService } from 'src/modules/delivery/providers/delivery.service';
|
import { DeliveryService } from 'src/modules/delivery/providers/delivery.service';
|
||||||
import { PaymentMethodService } from 'src/modules/payments/services/payment-method.service';
|
import { PaymentMethodService } from 'src/modules/payments/services/payment-method.service';
|
||||||
import { Variant } from 'src/modules/products/entities/variant.entity';
|
import { Variant } from 'src/modules/products/entities/variant.entity';
|
||||||
|
import { getMinQuantity, isValidProductQuantity, roundQuantity } from 'src/modules/products/utils/quantity.utils';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CartValidationService {
|
export class CartValidationService {
|
||||||
@@ -53,6 +54,8 @@ export class CartValidationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.assertValidQuantity(variant, existingQuantityInCart + quantityToAdd);
|
||||||
|
|
||||||
const maxPurchase = variant.product.maxPurchaseQuantity;
|
const maxPurchase = variant.product.maxPurchaseQuantity;
|
||||||
if (maxPurchase != null && maxPurchase > 0) {
|
if (maxPurchase != null && maxPurchase > 0) {
|
||||||
const productVariantIds = variant.product.variants.getItems().map((v) => v.id);
|
const productVariantIds = variant.product.variants.getItems().map((v) => v.id);
|
||||||
@@ -70,6 +73,20 @@ export class CartValidationService {
|
|||||||
return variant;
|
return variant;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assertValidQuantity(variant: Variant, totalQuantity: number): void {
|
||||||
|
const product = variant.product;
|
||||||
|
const quantity = roundQuantity(totalQuantity);
|
||||||
|
|
||||||
|
if (!isValidProductQuantity(product, quantity)) {
|
||||||
|
throw new BadRequestException(CartMessage.QUANTITY_STEP_MISMATCH);
|
||||||
|
}
|
||||||
|
|
||||||
|
const minPurchase = getMinQuantity(product);
|
||||||
|
if (quantity < minPurchase) {
|
||||||
|
throw new BadRequestException(CartMessage.MIN_PURCHASE_NOT_MET);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate address belongs to user
|
* Validate address belongs to user
|
||||||
|
|||||||
@@ -153,6 +153,15 @@ export class CartService {
|
|||||||
return cart;
|
return cart;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increment item quantity in cart by product purchase step
|
||||||
|
*/
|
||||||
|
async incrementItemByStep(userId: string, shopId: string, variantId: string): Promise<Cart> {
|
||||||
|
const cart = await this.getOrCreateCart(userId, shopId);
|
||||||
|
await this.itemService.incrementByStep(cart, variantId, shopId);
|
||||||
|
return this.recalculateAndSaveCart(cart);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Increment item quantity in cart
|
* Increment item quantity in cart
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export class OrderItem extends BaseEntity {
|
|||||||
@ManyToOne(() => Variant)
|
@ManyToOne(() => Variant)
|
||||||
variant!: Variant;
|
variant!: Variant;
|
||||||
|
|
||||||
@Property({ type: 'int' })
|
@Property({ type: 'decimal', precision: 10, scale: 3 })
|
||||||
quantity!: number;
|
quantity!: number;
|
||||||
|
|
||||||
@Property({ type: 'decimal', precision: 10, scale: 0 })
|
@Property({ type: 'decimal', precision: 10, scale: 0 })
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export class Order extends BaseEntity {
|
|||||||
@Property({ type: 'decimal', precision: 10, scale: 0 })
|
@Property({ type: 'decimal', precision: 10, scale: 0 })
|
||||||
total!: number;
|
total!: number;
|
||||||
|
|
||||||
@Property({ type: 'int', default: 0 })
|
@Property({ type: 'decimal', precision: 10, scale: 3, default: 0 })
|
||||||
totalItems: number = 0;
|
totalItems: number = 0;
|
||||||
|
|
||||||
@Property({ type: 'text', nullable: true })
|
@Property({ type: 'text', nullable: true })
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { ShopService } from 'src/modules/shops/providers/shops.service';
|
|||||||
import { DeliveryService } from 'src/modules/delivery/providers/delivery.service';
|
import { DeliveryService } from 'src/modules/delivery/providers/delivery.service';
|
||||||
import { PaymentMethodService } from 'src/modules/payments/services/payment-method.service';
|
import { PaymentMethodService } from 'src/modules/payments/services/payment-method.service';
|
||||||
import { OrderItemStatus } from '../enum/order-item.enum';
|
import { OrderItemStatus } from '../enum/order-item.enum';
|
||||||
|
import { getMinQuantity, isValidProductQuantity, roundQuantity } from '../../products/utils/quantity.utils';
|
||||||
|
|
||||||
type OrderItemData = { variant: Variant; quantity: number; unitPrice: number; discount: number };
|
type OrderItemData = { variant: Variant; quantity: number; unitPrice: number; discount: number };
|
||||||
|
|
||||||
@@ -67,6 +68,11 @@ export class OrdersService {
|
|||||||
const validated = await this.validateCartForOrder(userId, shopId, cart);
|
const validated = await this.validateCartForOrder(userId, shopId, cart);
|
||||||
|
|
||||||
const order = await this.em.transactional(async em => {
|
const order = await this.em.transactional(async em => {
|
||||||
|
const needsConfirmation = validated.orderItemsData.some(
|
||||||
|
({ variant }) => variant.product.needAdminAcceptance,
|
||||||
|
);
|
||||||
|
const initialStatus = needsConfirmation ? OrderStatus.NEED_CONFIRMATION : OrderStatus.PENDING_PAYMENT;
|
||||||
|
|
||||||
const order = em.create(Order, {
|
const order = em.create(Order, {
|
||||||
user: validated.user,
|
user: validated.user,
|
||||||
shop: validated.shop,
|
shop: validated.shop,
|
||||||
@@ -83,8 +89,8 @@ export class OrdersService {
|
|||||||
total: cart.total || 0,
|
total: cart.total || 0,
|
||||||
totalItems: cart.totalItems || 0,
|
totalItems: cart.totalItems || 0,
|
||||||
description: cart.description,
|
description: cart.description,
|
||||||
status: OrderStatus.PENDING_PAYMENT,
|
status: initialStatus,
|
||||||
history: [{ status: OrderStatus.PENDING_PAYMENT, changedAt: new Date() }],
|
history: [{ status: initialStatus, changedAt: new Date() }],
|
||||||
});
|
});
|
||||||
|
|
||||||
em.persist(order);
|
em.persist(order);
|
||||||
@@ -101,7 +107,9 @@ export class OrdersService {
|
|||||||
unitPrice,
|
unitPrice,
|
||||||
discount,
|
discount,
|
||||||
totalPrice,
|
totalPrice,
|
||||||
status: OrderItemStatus.normal,
|
status: variant.product.needAdminAcceptance
|
||||||
|
? OrderItemStatus.needConfirmation
|
||||||
|
: OrderItemStatus.normal,
|
||||||
});
|
});
|
||||||
|
|
||||||
em.persist(orderItem);
|
em.persist(orderItem);
|
||||||
@@ -366,11 +374,19 @@ export class OrdersService {
|
|||||||
throw new BadRequestException(CartMessage.INSUFFICIENT_STOCK);
|
throw new BadRequestException(CartMessage.INSUFFICIENT_STOCK);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const quantity = roundQuantity(cartItem.quantity);
|
||||||
|
if (!isValidProductQuantity(variant.product, quantity)) {
|
||||||
|
throw new BadRequestException(CartMessage.QUANTITY_STEP_MISMATCH);
|
||||||
|
}
|
||||||
|
if (quantity < getMinQuantity(variant.product)) {
|
||||||
|
throw new BadRequestException(CartMessage.MIN_PURCHASE_NOT_MET);
|
||||||
|
}
|
||||||
|
|
||||||
const maxPurchase = variant.product.maxPurchaseQuantity;
|
const maxPurchase = variant.product.maxPurchaseQuantity;
|
||||||
if (maxPurchase != null && maxPurchase > 0) {
|
if (maxPurchase != null && maxPurchase > 0) {
|
||||||
const productId = variant.product.id;
|
const productId = variant.product.id;
|
||||||
const currentProductTotal = productQuantityMap.get(productId) ?? 0;
|
const currentProductTotal = productQuantityMap.get(productId) ?? 0;
|
||||||
const newProductTotal = currentProductTotal + cartItem.quantity;
|
const newProductTotal = currentProductTotal + quantity;
|
||||||
if (newProductTotal > maxPurchase) {
|
if (newProductTotal > maxPurchase) {
|
||||||
throw new BadRequestException(CartMessage.MAX_PURCHASE_EXCEEDED);
|
throw new BadRequestException(CartMessage.MAX_PURCHASE_EXCEEDED);
|
||||||
}
|
}
|
||||||
@@ -379,7 +395,7 @@ export class OrdersService {
|
|||||||
|
|
||||||
orderItemsData.push({
|
orderItemsData.push({
|
||||||
variant,
|
variant,
|
||||||
quantity: cartItem.quantity,
|
quantity,
|
||||||
unitPrice: variant.price || 0,
|
unitPrice: variant.price || 0,
|
||||||
discount: variant.product.discount || 0,
|
discount: variant.product.discount || 0,
|
||||||
});
|
});
|
||||||
@@ -490,7 +506,7 @@ export class OrdersService {
|
|||||||
SELECT
|
SELECT
|
||||||
f.id as food_id,
|
f.id as food_id,
|
||||||
f.title as food_title,
|
f.title as food_title,
|
||||||
COALESCE(SUM(oi.quantity), 0)::int as total_quantity,
|
COALESCE(SUM(oi.quantity), 0)::numeric as total_quantity,
|
||||||
COALESCE(SUM(oi.total_price), 0)::numeric as total_revenue
|
COALESCE(SUM(oi.total_price), 0)::numeric as total_revenue
|
||||||
FROM order_items oi
|
FROM order_items oi
|
||||||
INNER JOIN orders o ON oi.order_id = o.id
|
INNER JOIN orders o ON oi.order_id = o.id
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export class CreateVariantDto {
|
|||||||
price: number;
|
price: number;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsInt()
|
@IsNumber({ maxDecimalPlaces: 3 })
|
||||||
@Min(0)
|
@Min(0)
|
||||||
@Type(() => Number)
|
@Type(() => Number)
|
||||||
@ApiPropertyOptional({ description: 'Available stock; omit or null for unlimited', example: 10 })
|
@ApiPropertyOptional({ description: 'Available stock; omit or null for unlimited', example: 10 })
|
||||||
@@ -119,17 +119,18 @@ export class CreateProductDto {
|
|||||||
order?: number;
|
order?: number;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsInt()
|
@IsNumber({ maxDecimalPlaces: 3 })
|
||||||
@Min(1)
|
@Min(0.001)
|
||||||
@Type(() => Number)
|
@Type(() => Number)
|
||||||
@ApiPropertyOptional({ description: 'Maximum quantity per order; omit or null for unlimited', example: 5 })
|
@ApiPropertyOptional({ description: 'Maximum quantity per order; omit or null for unlimited', example: 5 })
|
||||||
maxPurchaseQuantity?: number | null;
|
maxPurchaseQuantity?: number | null;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsNumber({ maxDecimalPlaces: 3 })
|
||||||
@ApiPropertyOptional({ example: '1000' })
|
@Min(0)
|
||||||
@Type(() => String)
|
@Type(() => Number)
|
||||||
purchasePitch?: string | null;
|
@ApiPropertyOptional({ example: 0.5 })
|
||||||
|
purchasePitch?: number | null;
|
||||||
|
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@IsEnum(PricingType)
|
@IsEnum(PricingType)
|
||||||
@@ -142,10 +143,10 @@ export class CreateProductDto {
|
|||||||
purchaseUnit?: PurchaseUnit;
|
purchaseUnit?: PurchaseUnit;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsInt()
|
@IsNumber({ maxDecimalPlaces: 3 })
|
||||||
@Min(1)
|
@Min(0.001)
|
||||||
@Type(() => Number)
|
@Type(() => Number)
|
||||||
@ApiPropertyOptional({ description: 'Minimum quantity per order; omit or null for unlimited', example: 1 })
|
@ApiPropertyOptional({ description: 'Minimum quantity per order; omit or null for unlimited', example: 0.5 })
|
||||||
minPurchaseQuantity?: number | null;
|
minPurchaseQuantity?: number | null;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,10 +58,10 @@ export class Product extends BaseEntity {
|
|||||||
|
|
||||||
|
|
||||||
/** Maximum quantity per order; null means unlimited */
|
/** Maximum quantity per order; null means unlimited */
|
||||||
@Property({ type: 'int', nullable: true, default: null })
|
@Property({ type: 'decimal', precision: 10, scale: 3, nullable: true, default: null })
|
||||||
maxPurchaseQuantity: number | null = null;
|
maxPurchaseQuantity: number | null = null;
|
||||||
|
|
||||||
@Property({ type: 'int', nullable: true, default: null })
|
@Property({ type: 'decimal', precision: 10, scale: 3, nullable: true, default: null })
|
||||||
minPurchaseQuantity: number | null = null;
|
minPurchaseQuantity: number | null = null;
|
||||||
|
|
||||||
// برای کالا هایی مثل گوشت متغیر میشه
|
// برای کالا هایی مثل گوشت متغیر میشه
|
||||||
@@ -79,7 +79,7 @@ export class Product extends BaseEntity {
|
|||||||
nullable: true,
|
nullable: true,
|
||||||
default: null,
|
default: null,
|
||||||
})
|
})
|
||||||
purchasePitch?: string | null = null;
|
purchasePitch?: number | null = null;
|
||||||
|
|
||||||
//
|
//
|
||||||
@Property({ type: 'boolean', default: false })
|
@Property({ type: 'boolean', default: false })
|
||||||
|
|||||||
@@ -15,6 +15,6 @@ export class Variant extends BaseEntity {
|
|||||||
price?: number;
|
price?: number;
|
||||||
|
|
||||||
/** Available quantity; null means unlimited */
|
/** Available quantity; null means unlimited */
|
||||||
@Property({ type: 'int', nullable: true, default: null })
|
@Property({ type: 'decimal', precision: 10, scale: 3, nullable: true, default: null })
|
||||||
stock: number | null = null;
|
stock: number | null = null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { Product } from '../entities/product.entity';
|
||||||
|
import { PricingType } from '../enum/pricing.enum';
|
||||||
|
|
||||||
|
const QUANTITY_SCALE = 3;
|
||||||
|
const SCALE_FACTOR = 10 ** QUANTITY_SCALE;
|
||||||
|
|
||||||
|
export function getQuantityStep(product: Product): number {
|
||||||
|
if (product.pricingType === PricingType.VARIABLE && product.purchasePitch != null && product.purchasePitch > 0) {
|
||||||
|
return product.purchasePitch;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMinQuantity(product: Product): number {
|
||||||
|
if (product.minPurchaseQuantity != null && product.minPurchaseQuantity > 0) {
|
||||||
|
return product.minPurchaseQuantity;
|
||||||
|
}
|
||||||
|
return product.pricingType === PricingType.VARIABLE ? getQuantityStep(product) : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function roundQuantity(value: number): number {
|
||||||
|
return Math.round(value * SCALE_FACTOR) / SCALE_FACTOR;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isQuantityAlignedToPitch(quantity: number, pitch: number): boolean {
|
||||||
|
if (pitch <= 0) return true;
|
||||||
|
const scaledQty = Math.round(roundQuantity(quantity) * SCALE_FACTOR);
|
||||||
|
const scaledPitch = Math.round(roundQuantity(pitch) * SCALE_FACTOR);
|
||||||
|
return scaledQty % scaledPitch === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isValidProductQuantity(product: Product, quantity: number): boolean {
|
||||||
|
const qty = roundQuantity(quantity);
|
||||||
|
if (qty <= 0) return false;
|
||||||
|
|
||||||
|
if (product.pricingType === PricingType.FIXED && !Number.isInteger(qty)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (product.pricingType === PricingType.VARIABLE && product.purchasePitch != null && product.purchasePitch > 0) {
|
||||||
|
return isQuantityAlignedToPitch(qty, product.purchasePitch);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user