update order as admin
This commit is contained in:
@@ -730,6 +730,8 @@ export const enum OrderMessage {
|
|||||||
PAYMENT_METHOD_NOT_ENABLED = 'روش پرداخت برای این رستوران فعال نیست',
|
PAYMENT_METHOD_NOT_ENABLED = 'روش پرداخت برای این رستوران فعال نیست',
|
||||||
PRODUCT_NOT_FOUND = 'محصول یافت نشد',
|
PRODUCT_NOT_FOUND = 'محصول یافت نشد',
|
||||||
PRODUCT_NOT_BELONGS_TO_SHOP = 'محصول به این فروشگاه تعلق ندارد',
|
PRODUCT_NOT_BELONGS_TO_SHOP = 'محصول به این فروشگاه تعلق ندارد',
|
||||||
|
ORDER_ITEM_NOT_FOUND = 'آیتم سفارش یافت نشد',
|
||||||
|
ORDER_NOT_EDITABLE = 'سفارش در این وضعیت قابل ویرایش نیست',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const enum CartMessage {
|
export const enum CartMessage {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { FindOrdersDto } from '../dto/find-orders.dto';
|
|||||||
import { OrderStatus } from '../interface/order.interface';
|
import { OrderStatus } from '../interface/order.interface';
|
||||||
import { API_HEADER_SLUG } from 'src/common/constants/index';
|
import { API_HEADER_SLUG } from 'src/common/constants/index';
|
||||||
import { UpdateOrderStatusDto } from '../dto/update-order-status.dto';
|
import { UpdateOrderStatusDto } from '../dto/update-order-status.dto';
|
||||||
|
import { UpdateOrderItemQuantityDto } from '../dto/update-order-item-quantity.dto';
|
||||||
import { Permissions } from 'src/common/decorators/permissions.decorator';
|
import { Permissions } from 'src/common/decorators/permissions.decorator';
|
||||||
import { Permission } from 'src/common/enums/permission.enum';
|
import { Permission } from 'src/common/enums/permission.enum';
|
||||||
|
|
||||||
@@ -94,6 +95,22 @@ export class OrdersController {
|
|||||||
return this.ordersService.changeOrderStatus(orderId, shopId, status, 'admin', dto?.desc);
|
return this.ordersService.changeOrderStatus(orderId, shopId, status, 'admin', dto?.desc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@UseGuards(AdminAuthGuard)
|
||||||
|
@Permissions(Permission.MANAGE_ORDERS)
|
||||||
|
@Patch('admin/orders/:orderId/items/:orderItemId/quantity')
|
||||||
|
@ApiOperation({ summary: 'Update order item quantity' })
|
||||||
|
@ApiParam({ name: 'orderId', description: 'Order ID' })
|
||||||
|
@ApiParam({ name: 'orderItemId', description: 'Order Item ID' })
|
||||||
|
@ApiBody({ type: UpdateOrderItemQuantityDto })
|
||||||
|
updateOrderItemQuantity(
|
||||||
|
@Param('orderId') orderId: string,
|
||||||
|
@Param('orderItemId') orderItemId: string,
|
||||||
|
@Body() dto: UpdateOrderItemQuantityDto,
|
||||||
|
@ShopId() shopId: string,
|
||||||
|
) {
|
||||||
|
return this.ordersService.updateOrderItemQuantity(orderId, orderItemId, shopId, dto.quantity);
|
||||||
|
}
|
||||||
|
|
||||||
@UseGuards(AdminAuthGuard)
|
@UseGuards(AdminAuthGuard)
|
||||||
@Permissions(Permission.VIEW_REPORTS)
|
@Permissions(Permission.VIEW_REPORTS)
|
||||||
@ApiOperation({ summary: 'Get Stats for report page' })
|
@ApiOperation({ summary: 'Get Stats for report page' })
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { IsNotEmpty, IsNumber, Min } from 'class-validator';
|
||||||
|
import { Type } from 'class-transformer';
|
||||||
|
|
||||||
|
export class UpdateOrderItemQuantityDto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'New quantity for the order item (supports decimals for variable products)',
|
||||||
|
example: 2,
|
||||||
|
minimum: 0.001,
|
||||||
|
})
|
||||||
|
@IsNotEmpty()
|
||||||
|
@Type(() => Number)
|
||||||
|
@IsNumber({ maxDecimalPlaces: 3 })
|
||||||
|
@Min(0.001)
|
||||||
|
quantity!: number;
|
||||||
|
}
|
||||||
@@ -23,11 +23,13 @@ import { NotificationsModule } from '../notifications/notifications.module';
|
|||||||
import { UserModule } from '../users/user.module';
|
import { UserModule } from '../users/user.module';
|
||||||
import { ShopsModule } from '../shops/shops.module';
|
import { ShopsModule } from '../shops/shops.module';
|
||||||
import { DeliveryModule } from '../delivery/delivery.module';
|
import { DeliveryModule } from '../delivery/delivery.module';
|
||||||
|
import { CouponModule } from '../coupons/coupon.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
MikroOrmModule.forFeature([Order, OrderItem, User, Shop, Product, Variant, UserAddress, PaymentMethod]),
|
MikroOrmModule.forFeature([Order, OrderItem, User, Shop, Product, Variant, UserAddress, PaymentMethod]),
|
||||||
forwardRef(() => CartModule),
|
forwardRef(() => CartModule),
|
||||||
|
CouponModule,
|
||||||
UtilsModule,
|
UtilsModule,
|
||||||
AuthModule,
|
AuthModule,
|
||||||
forwardRef(() => PaymentsModule),
|
forwardRef(() => PaymentsModule),
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ 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';
|
import { getMinQuantity, isValidProductQuantity, roundQuantity } from '../../products/utils/quantity.utils';
|
||||||
|
import { CouponService } from 'src/modules/coupons/providers/coupon.service';
|
||||||
|
import { CouponType } from 'src/modules/coupons/interface/coupon';
|
||||||
|
|
||||||
type OrderItemData = { variant: Variant; quantity: number; unitPrice: number; discount: number };
|
type OrderItemData = { variant: Variant; quantity: number; unitPrice: number; discount: number };
|
||||||
|
|
||||||
@@ -61,6 +63,7 @@ export class OrdersService {
|
|||||||
private readonly paymentMethodService: PaymentMethodService,
|
private readonly paymentMethodService: PaymentMethodService,
|
||||||
private readonly eventEmitter: EventEmitter2,
|
private readonly eventEmitter: EventEmitter2,
|
||||||
private readonly userService: UserService,
|
private readonly userService: UserService,
|
||||||
|
private readonly couponService: CouponService,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
async checkout(userId: string, shopId: string) {
|
async checkout(userId: string, shopId: string) {
|
||||||
@@ -251,7 +254,153 @@ export class OrdersService {
|
|||||||
);
|
);
|
||||||
return order;
|
return order;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateOrderItemQuantity(
|
||||||
|
orderId: string,
|
||||||
|
orderItemId: string,
|
||||||
|
shopId: string,
|
||||||
|
quantity: number,
|
||||||
|
): Promise<Order> {
|
||||||
|
const order = await this.em.findOne(
|
||||||
|
Order,
|
||||||
|
{ id: orderId, shop: { id: shopId } },
|
||||||
|
{
|
||||||
|
populate: [
|
||||||
|
'shop',
|
||||||
|
'items',
|
||||||
|
'items.variant',
|
||||||
|
'items.variant.product',
|
||||||
|
'items.variant.product.shop',
|
||||||
|
'items.variant.product.variants',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (!order) {
|
||||||
|
throw new NotFoundException(OrderMessage.NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.assertOrderEditable(order);
|
||||||
|
|
||||||
|
const orderItem = order.items.getItems().find(item => item.id === orderItemId);
|
||||||
|
if (!orderItem) {
|
||||||
|
throw new NotFoundException(OrderMessage.ORDER_ITEM_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedQuantity = roundQuantity(quantity);
|
||||||
|
this.assertValidOrderItemQuantity(order, orderItem, shopId, normalizedQuantity);
|
||||||
|
|
||||||
|
orderItem.quantity = normalizedQuantity;
|
||||||
|
orderItem.totalPrice = (Number(orderItem.unitPrice) - Number(orderItem.discount)) * normalizedQuantity;
|
||||||
|
|
||||||
|
await this.recalculateOrderTotals(order);
|
||||||
|
await this.em.flush();
|
||||||
|
|
||||||
|
return this.findOne(orderId, shopId);
|
||||||
|
}
|
||||||
|
|
||||||
/* Helpers */
|
/* Helpers */
|
||||||
|
private assertOrderEditable(order: Order) {
|
||||||
|
if ([OrderStatus.COMPLETED, OrderStatus.CANCELED].includes(order.status)) {
|
||||||
|
throw new BadRequestException(OrderMessage.ORDER_NOT_EDITABLE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private assertValidOrderItemQuantity(
|
||||||
|
order: Order,
|
||||||
|
orderItem: OrderItem,
|
||||||
|
shopId: string,
|
||||||
|
quantity: number,
|
||||||
|
) {
|
||||||
|
const variant = orderItem.variant;
|
||||||
|
|
||||||
|
if (variant.product.shop.id !== shopId) {
|
||||||
|
throw new BadRequestException(OrderMessage.PRODUCT_NOT_BELONGS_TO_SHOP);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (quantity < getMinQuantity(variant.product)) {
|
||||||
|
throw new BadRequestException(CartMessage.MIN_PURCHASE_NOT_MET);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private async recalculateOrderTotals(order: Order): Promise<void> {
|
||||||
|
let subTotal = 0;
|
||||||
|
let itemsDiscount = 0;
|
||||||
|
let totalItems = 0;
|
||||||
|
|
||||||
|
for (const item of order.items.getItems()) {
|
||||||
|
const unitPrice = Number(item.unitPrice) || 0;
|
||||||
|
const unitDiscount = Math.min(Number(item.discount) || 0, unitPrice);
|
||||||
|
subTotal += unitPrice * item.quantity;
|
||||||
|
itemsDiscount += unitDiscount * item.quantity;
|
||||||
|
totalItems += item.quantity;
|
||||||
|
item.totalPrice = (unitPrice - unitDiscount) * item.quantity;
|
||||||
|
}
|
||||||
|
|
||||||
|
order.subTotal = subTotal;
|
||||||
|
order.itemsDiscount = itemsDiscount;
|
||||||
|
order.totalItems = totalItems;
|
||||||
|
order.couponDiscount = await this.calculateOrderCouponDiscount(order);
|
||||||
|
order.totalDiscount = order.couponDiscount + order.itemsDiscount;
|
||||||
|
|
||||||
|
const amountAfterDiscounts = Math.max(0, subTotal - order.totalDiscount);
|
||||||
|
const vat = order.shop?.vat ? Number(order.shop.vat) : 0;
|
||||||
|
order.tax = vat > 0 ? (amountAfterDiscounts * vat) / 100 : 0;
|
||||||
|
order.total = amountAfterDiscounts + order.tax + Number(order.deliveryFee || 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async calculateOrderCouponDiscount(order: Order): Promise<number> {
|
||||||
|
const couponDetail = order.couponDetail;
|
||||||
|
if (!couponDetail) return 0;
|
||||||
|
|
||||||
|
let eligibleSubTotal = order.subTotal;
|
||||||
|
let eligibleItemsDiscount = order.itemsDiscount;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const coupon = await this.couponService.findById(couponDetail.couponId);
|
||||||
|
const hasCategoryRestriction = (coupon.productCategories?.length ?? 0) > 0;
|
||||||
|
const hasFoodRestriction = (coupon.products?.length ?? 0) > 0;
|
||||||
|
|
||||||
|
if (hasCategoryRestriction || hasFoodRestriction) {
|
||||||
|
eligibleSubTotal = 0;
|
||||||
|
eligibleItemsDiscount = 0;
|
||||||
|
|
||||||
|
for (const item of order.items.getItems()) {
|
||||||
|
const product = item.variant.product;
|
||||||
|
await this.em.populate(product, ['category']);
|
||||||
|
|
||||||
|
const matchesCategory =
|
||||||
|
hasCategoryRestriction &&
|
||||||
|
product.category &&
|
||||||
|
(coupon.productCategories ?? []).includes(product.category.id);
|
||||||
|
const matchesFood = hasFoodRestriction && (coupon.products ?? []).includes(product.id);
|
||||||
|
|
||||||
|
if (matchesCategory || matchesFood) {
|
||||||
|
const unitPrice = Number(item.unitPrice) || 0;
|
||||||
|
const unitDiscount = Math.min(Number(item.discount) || 0, unitPrice);
|
||||||
|
eligibleSubTotal += unitPrice * item.quantity;
|
||||||
|
eligibleItemsDiscount += unitDiscount * item.quantity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Fall back to order-wide totals when coupon lookup fails.
|
||||||
|
}
|
||||||
|
|
||||||
|
const priceAfterItemDiscount = Math.max(0, eligibleSubTotal - eligibleItemsDiscount);
|
||||||
|
|
||||||
|
if (couponDetail.type === CouponType.PERCENTAGE) {
|
||||||
|
let discount = (priceAfterItemDiscount * couponDetail.value) / 100;
|
||||||
|
if (couponDetail.maxDiscount && discount > couponDetail.maxDiscount) {
|
||||||
|
discount = couponDetail.maxDiscount;
|
||||||
|
}
|
||||||
|
return discount;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.min(couponDetail.value, priceAfterItemDiscount);
|
||||||
|
}
|
||||||
|
|
||||||
private assertStatusTransitionAllowed(order: Order, to: OrderStatus, ref: StatusTransitionRef) {
|
private assertStatusTransitionAllowed(order: Order, to: OrderStatus, ref: StatusTransitionRef) {
|
||||||
const paymentMethod = order.paymentMethod?.method;
|
const paymentMethod = order.paymentMethod?.method;
|
||||||
if (!paymentMethod) {
|
if (!paymentMethod) {
|
||||||
|
|||||||
Reference in New Issue
Block a user