add discount

This commit is contained in:
2026-07-01 09:47:58 +03:30
parent 5d0e862259
commit 77acab935a
5 changed files with 48 additions and 10 deletions
+2 -1
View File
@@ -4143,7 +4143,7 @@
"unsigned": false, "unsigned": false,
"autoincrement": false, "autoincrement": false,
"primary": false, "primary": false,
"nullable": false, "nullable": true,
"length": 26, "length": 26,
"mappedType": "character" "mappedType": "character"
}, },
@@ -4431,6 +4431,7 @@
"id" "id"
], ],
"referencedTableName": "public.users", "referencedTableName": "public.users",
"deleteRule": "set null",
"updateRule": "cascade" "updateRule": "cascade"
}, },
"orders_restaurant_id_foreign": { "orders_restaurant_id_foreign": {
@@ -0,0 +1,21 @@
import { Migration } from '@mikro-orm/migrations';
export class Migration20260701052836_optionalUserForOrder extends Migration {
override async up(): Promise<void> {
this.addSql(`alter table "orders" drop constraint "orders_user_id_foreign";`);
this.addSql(`alter table "orders" alter column "user_id" type char(26) using ("user_id"::char(26));`);
this.addSql(`alter table "orders" alter column "user_id" drop not null;`);
this.addSql(`alter table "orders" add constraint "orders_user_id_foreign" foreign key ("user_id") references "users" ("id") on update cascade on delete set null;`);
}
override async down(): Promise<void> {
this.addSql(`alter table "orders" drop constraint "orders_user_id_foreign";`);
this.addSql(`alter table "orders" alter column "user_id" type char(26) using ("user_id"::char(26));`);
this.addSql(`alter table "orders" alter column "user_id" set not null;`);
this.addSql(`alter table "orders" add constraint "orders_user_id_foreign" foreign key ("user_id") references "users" ("id") on update cascade;`);
}
}
+2
View File
@@ -677,6 +677,7 @@ export const enum GroupMessage {
} }
export const enum OrderMessage { export const enum OrderMessage {
USER_REQUIRED = 'کاربر الزامی است',
NOT_FOUND = 'سفارش یافت نشد', NOT_FOUND = 'سفارش یافت نشد',
PAYMENT_METHOD_MISSING = 'روش پرداخت سفارش مشخص نشده است', PAYMENT_METHOD_MISSING = 'روش پرداخت سفارش مشخص نشده است',
INVALID_STATUS_TRANSITION = 'انتقال وضعیت نامعتبر است', INVALID_STATUS_TRANSITION = 'انتقال وضعیت نامعتبر است',
@@ -695,6 +696,7 @@ export const enum OrderMessage {
FOOD_NOT_FOUND = 'غذا یافت نشد', FOOD_NOT_FOUND = 'غذا یافت نشد',
FOOD_NOT_BELONGS_TO_RESTAURANT = 'غذا به این رستوران تعلق ندارد', FOOD_NOT_BELONGS_TO_RESTAURANT = 'غذا به این رستوران تعلق ندارد',
CREDIT_CARD_PAYMENT_DETAILS_REQUIRED = 'حداقل یکی از توضیحات پرداخت یا پیوست رسید کارت به کارت الزامی است', CREDIT_CARD_PAYMENT_DETAILS_REQUIRED = 'حداقل یکی از توضیحات پرداخت یا پیوست رسید کارت به کارت الزامی است',
DISCOUNT_AMOUNT_EXCEEDS_ORDER_TOTAL = 'مبلغ تخفیف بیشتر از مبلغ سفارش است',
} }
export const enum CartMessage { export const enum CartMessage {
@@ -25,10 +25,10 @@ export class AdminOrderItemDto {
} }
export class AdminCreateOrderDto { export class AdminCreateOrderDto {
@ApiProperty({ description: 'User ID to create the order for' }) @ApiPropertyOptional({ description: 'User ID to create the order for' })
@IsString() @IsString()
@IsNotEmpty() @IsOptional()
userId: string; userId?: string;
@ApiProperty({ type: [AdminOrderItemDto], description: 'Order items' }) @ApiProperty({ type: [AdminOrderItemDto], description: 'Order items' })
@IsArray() @IsArray()
@@ -47,6 +47,12 @@ export class AdminCreateOrderDto {
@IsNotEmpty() @IsNotEmpty()
paymentMethodId: string; paymentMethodId: string;
@ApiPropertyOptional({ description: 'Extra discount amount to apply on the order', minimum: 0 })
@IsOptional()
@IsNumber()
@Min(0)
discountAmount?: number;
@ApiPropertyOptional({ description: 'Order description or notes' }) @ApiPropertyOptional({ description: 'Order description or notes' })
@IsOptional() @IsOptional()
@IsString() @IsString()
+14 -6
View File
@@ -67,7 +67,6 @@ export class OrdersService {
async checkout(userId: string, restaurantId: string) { async checkout(userId: string, restaurantId: string) {
const cart = await this.cartService.findOneOrFail(userId, restaurantId); const cart = await this.cartService.findOneOrFail(userId, restaurantId);
const validated = await this.validateCartForOrder(userId, restaurantId, cart); const validated = await this.validateCartForOrder(userId, restaurantId, cart);
console.log(cart);
const order = await this.em.transactional(async em => { const order = await this.em.transactional(async em => {
const order = em.create(Order, { const order = em.create(Order, {
@@ -148,7 +147,7 @@ console.log(cart);
} }
async createOrderForUser(restaurantId: string, dto: AdminCreateOrderDto): Promise<Order> { async createOrderForUser(restaurantId: string, dto: AdminCreateOrderDto): Promise<Order> {
const user = await this.getUserOrFail(dto.userId); const user = dto.userId ? await this.getUserOrFail(dto.userId) : null;
const [restaurant, delivery] = await Promise.all([ const [restaurant, delivery] = await Promise.all([
this.getRestaurantOrFail(restaurantId), this.getRestaurantOrFail(restaurantId),
@@ -162,7 +161,7 @@ console.log(cart);
const userAddress = const userAddress =
delivery.method === DeliveryMethodEnum.DeliveryCourier delivery.method === DeliveryMethodEnum.DeliveryCourier
? await this.resolveUserAddressForOrder(user, dto.addressId!) ? await this.resolveUserAddressForOrder(user!, dto.addressId!)
: null; : null;
const orderItemsData = await this.buildOrderItemsDataFromDto(dto.items, restaurantId); const orderItemsData = await this.buildOrderItemsDataFromDto(dto.items, restaurantId);
@@ -170,7 +169,12 @@ console.log(cart);
const subTotal = orderItemsData.reduce((sum, item) => sum + item.unitPrice * item.quantity, 0); const subTotal = orderItemsData.reduce((sum, item) => sum + item.unitPrice * item.quantity, 0);
const itemsDiscount = orderItemsData.reduce((sum, item) => sum + item.discount * item.quantity, 0); const itemsDiscount = orderItemsData.reduce((sum, item) => sum + item.discount * item.quantity, 0);
const deliveryFee = delivery.deliveryFeeType === DeliveryFeeTypeEnum.FIXED ? Number(delivery.deliveryFee) : 0; const deliveryFee = delivery.deliveryFeeType === DeliveryFeeTypeEnum.FIXED ? Number(delivery.deliveryFee) : 0;
const totalDiscount = itemsDiscount; const adminDiscount = dto.discountAmount ?? 0;
const maxDiscount = subTotal - itemsDiscount;
if (adminDiscount > maxDiscount) {
throw new BadRequestException(OrderMessage.DISCOUNT_AMOUNT_EXCEEDS_ORDER_TOTAL);
}
const totalDiscount = itemsDiscount + adminDiscount;
const total = subTotal - totalDiscount + deliveryFee; const total = subTotal - totalDiscount + deliveryFee;
const totalItems = orderItemsData.reduce((sum, item) => sum + item.quantity, 0); const totalItems = orderItemsData.reduce((sum, item) => sum + item.quantity, 0);
@@ -182,7 +186,7 @@ console.log(cart);
userAddress, userAddress,
carAddress: delivery.method === DeliveryMethodEnum.DeliveryCar ? (dto.carAddress ?? null) : null, carAddress: delivery.method === DeliveryMethodEnum.DeliveryCar ? (dto.carAddress ?? null) : null,
paymentMethod, paymentMethod,
couponDiscount: 0, couponDiscount: adminDiscount,
couponDetail: null, couponDetail: null,
itemsDiscount, itemsDiscount,
totalDiscount, totalDiscount,
@@ -222,7 +226,7 @@ console.log(cart);
await this.inventoryService.deductFromInventory(em, bulkReserveFoodDto); await this.inventoryService.deductFromInventory(em, bulkReserveFoodDto);
await em.flush(); await em.flush();
this.logger.debug(`Admin created order ${order.id} for user ${user.id} (restaurant: ${restaurantId})`); this.logger.debug(`Admin created order ${order.id} for user ${user?.id || 'anonymous'} (restaurant: ${restaurantId})`);
return order; return order;
}); });
@@ -639,6 +643,10 @@ console.log(cart);
if (delivery.method === DeliveryMethodEnum.DeliveryCar && !dto.carAddress) { if (delivery.method === DeliveryMethodEnum.DeliveryCar && !dto.carAddress) {
throw new BadRequestException(OrderMessage.CAR_ADDRESS_REQUIRED); throw new BadRequestException(OrderMessage.CAR_ADDRESS_REQUIRED);
} }
if (delivery.method === DeliveryMethodEnum.DeliveryCourier && !dto.userId) {
throw new BadRequestException(OrderMessage.USER_REQUIRED);
}
} }