From 316aeef631c67fc05b5620942890df624819ccc6 Mon Sep 17 00:00:00 2001 From: nfesta2023 <142601504+nfesta2023@users.noreply.github.com> Date: Wed, 13 Mar 2024 20:32:45 +0700 Subject: [PATCH] apply promotion code (#71) Co-authored-by: NHT --- src/entity/coupon.entity.ts | 19 ++++- src/enum/index.ts | 5 ++ .../dto/apply-promotion-code-request.dto.ts | 11 +++ .../dto/apply-promotion-code-response.dto.ts | 13 ++++ src/feature/order/order.controller.ts | 56 +++++++++++++++ src/feature/order/order.service.ts | 69 ++++++++++++++++++- src/type/index.ts | 7 ++ 7 files changed, 175 insertions(+), 5 deletions(-) create mode 100644 src/feature/order/dto/apply-promotion-code-request.dto.ts create mode 100644 src/feature/order/dto/apply-promotion-code-response.dto.ts diff --git a/src/entity/coupon.entity.ts b/src/entity/coupon.entity.ts index 1f47b41..1082c81 100644 --- a/src/entity/coupon.entity.ts +++ b/src/entity/coupon.entity.ts @@ -1,4 +1,9 @@ -import { CouponCreatorType, CouponFilterType, CouponType } from 'src/enum'; +import { + CalculationType, + CouponCreatorType, + CouponFilterType, + CouponType, +} from 'src/enum'; import { Column, Entity, @@ -38,8 +43,16 @@ export class Coupon { @Column({ type: 'int', nullable: false, unique: false }) public discount_value: number; - @Column({ type: 'int', nullable: false, unique: false }) - public discount_unit: number; + // @Column({ type: 'int', nullable: false, unique: false }) + // public discount_unit: number; + + @Column({ + type: 'enum', + enum: CalculationType, + nullable: false, + unique: false, + }) + public calculation_type: CalculationType; @Column({ type: 'tinyint', nullable: false, unique: false }) public is_active: number; diff --git a/src/enum/index.ts b/src/enum/index.ts index 8fdc487..ba22cad 100644 --- a/src/enum/index.ts +++ b/src/enum/index.ts @@ -56,3 +56,8 @@ export enum CouponFilterType { INCLUDED = 'included', EXCLUDED = 'excluded', } + +export enum CalculationType { + PERCENTAGE = 'percentage', + FIXED = 'fixed', +} diff --git a/src/feature/order/dto/apply-promotion-code-request.dto.ts b/src/feature/order/dto/apply-promotion-code-request.dto.ts new file mode 100644 index 0000000..d2a7e92 --- /dev/null +++ b/src/feature/order/dto/apply-promotion-code-request.dto.ts @@ -0,0 +1,11 @@ +export class ApplyPromotionCodeRequest { + coupon_code: string; + restaurant_id: number; + items: CouponAppliedItem[]; +} +interface CouponAppliedItem { + sku_id: number; + qty_ordered: number; + price_after_discount: number; + packaging_price: number; +} diff --git a/src/feature/order/dto/apply-promotion-code-response.dto.ts b/src/feature/order/dto/apply-promotion-code-response.dto.ts new file mode 100644 index 0000000..5a16f33 --- /dev/null +++ b/src/feature/order/dto/apply-promotion-code-response.dto.ts @@ -0,0 +1,13 @@ +export class ApplyPromotionCodeResponse { + discount_amount: number; + currency: string; + coupon_code: string; + restaurant_id: number; + items: CouponAppliedItem[]; +} +interface CouponAppliedItem { + sku_id: number; + qty_ordered: number; + price_after_discount: number; + packaging_price: number; +} diff --git a/src/feature/order/order.controller.ts b/src/feature/order/order.controller.ts index ccea22e..f5c2c50 100644 --- a/src/feature/order/order.controller.ts +++ b/src/feature/order/order.controller.ts @@ -16,6 +16,8 @@ import { GetCouponInfoRequest } from './dto/get-coupon-info-request.dto'; import { GetCouponInfoResponse } from './dto/get-coupon-info-response.dto'; import { Coupon } from 'src/entity/coupon.entity'; import { CustomRpcException } from 'src/exceptions/custom-rpc.exception'; +import { ApplyPromotionCodeRequest } from './dto/apply-promotion-code-request.dto'; +import { ApplyPromotionCodeResponse } from './dto/apply-promotion-code-response.dto'; @Controller('order') export class OrderController { @@ -142,4 +144,58 @@ export class OrderController { return result; } + + @MessagePattern({ cmd: 'apply_promotion_code' }) + @UseFilters(new CustomRpcExceptionFilter()) + async applyPromotionCode( + data: ApplyPromotionCodeRequest, + ): Promise { + const { coupon_code, restaurant_id, items } = data; + const result = new ApplyPromotionCodeResponse(); + const skuIds = items.map((i) => i.sku_id); + //Validate restaurant exist + const restaurant = + await this.commonService.getRestaurantById(restaurant_id); + if (!restaurant) { + throw new CustomRpcException(2, 'Restaurant doesnot exist'); + } + + //Validate SKU list belongs to the restaurant + if (items.length > 0) { + const isValidSkuList = + await this.commonService.validateSkuListBelongsToRestaurant( + restaurant_id, + skuIds, + ); + if (!isValidSkuList) { + throw new CustomRpcException( + 3, + 'item list does not belong to the resturant', + ); + } + } + const validCoupon = await this.orderService.validateApplyingCouponCode( + coupon_code, + restaurant_id, + skuIds, + ); + if (!validCoupon) { + throw new CustomRpcException(4, 'Coupon code is invalid'); + } + + const discountAmount: number = this.orderService.calculateDiscountAmount( + validCoupon, + items, + ); + + const unit = await this.orderService.getUnitById(restaurant.unit); + + result.discount_amount = discountAmount; + result.currency = unit.symbol; + result.coupon_code = coupon_code; + result.restaurant_id = restaurant_id; + result.items = items; + + return result; + } } diff --git a/src/feature/order/order.service.ts b/src/feature/order/order.service.ts index fb3ae76..2bdc214 100644 --- a/src/feature/order/order.service.ts +++ b/src/feature/order/order.service.ts @@ -5,14 +5,20 @@ import { } from '@nestjs/common'; import { InjectEntityManager, InjectRepository } from '@nestjs/typeorm'; import { Order } from 'src/entity/order.entity'; -import { CouponFilterType, CouponType, OrderStatus } from 'src/enum'; +import { + CalculationType, + CouponFilterType, + CouponType, + OrderStatus, +} from 'src/enum'; import { EntityManager, Repository } from 'typeorm'; import { GetApplicationFeeResponse } from './dto/get-application-fee-response.dto'; import { PaymentOption } from 'src/entity/payment-option.entity'; -import { MoneyType } from 'src/type'; +import { CouponAppliedItem, MoneyType } from 'src/type'; import { Restaurant } from 'src/entity/restaurant.entity'; import { CustomRpcException } from 'src/exceptions/custom-rpc.exception'; import { Coupon } from 'src/entity/coupon.entity'; +import { Unit } from 'src/entity/unit.entity'; @Injectable() export class OrderService { @@ -317,4 +323,63 @@ export class OrderService { return filteredCoupons; } + + async validateApplyingCouponCode( + coupon_code: string, + restaurant_id: number, + sku_ids: number[], + ): Promise { + let validCoupon: Coupon; + //Get restaurant coupon + const restaurantCoupons: Coupon[] = + await this.getCouponInfoWithRestaurntIds([restaurant_id]); + validCoupon = restaurantCoupons.find((i) => i.coupon_code == coupon_code); + if (validCoupon) { + return validCoupon; + } + //get sku coupon + const skuCoupons: Coupon[] = await this.getCouponInfoWithSkus(sku_ids); + + validCoupon = skuCoupons.find((i) => i.coupon_code == coupon_code); + if (validCoupon) { + return validCoupon; + } + + return validCoupon; + } + + calculateDiscountAmount(coupon: Coupon, items: CouponAppliedItem[]): number { + let discountAmount: number = 0; + //calculate the amount base to apply promotion code + let amount_base: number = 0; + for (const item of items) { + amount_base += + (item.price_after_discount + item.packaging_price) * item.qty_ordered; + } + + // check if the amount base is greater than minimum_order_value + if (amount_base < coupon.mininum_order_value) { + //the order does not reach the minimum order value + throw new CustomRpcException(5, { + minium_order_value: coupon.mininum_order_value, + }); + } + + //calculate the discount amount + if (coupon.calculation_type === CalculationType.PERCENTAGE) { + discountAmount = amount_base * (coupon.discount_value / 100); + discountAmount = Math.min(discountAmount, coupon.maximum_discount_amount); + } else if (coupon.calculation_type === CalculationType.FIXED) { + discountAmount = coupon.discount_value; + } + + return discountAmount; + } + + async getUnitById(unit_id: number): Promise { + return await this.entityManager + .createQueryBuilder(Unit, 'unit') + .where('unit.unit_id = :unit_id', { unit_id }) + .getOne(); + } } diff --git a/src/type/index.ts b/src/type/index.ts index 6a395e3..8d760bd 100644 --- a/src/type/index.ts +++ b/src/type/index.ts @@ -197,3 +197,10 @@ export interface MoneyType { amount: number; currency: string; } + +export interface CouponAppliedItem { + sku_id: number; + qty_ordered: number; + price_after_discount: number; + packaging_price: number; +}