import { randomBytes } from "crypto"; import { inject, injectable } from "inversify"; import { ClientSession, isValidObjectId, startSession } from "mongoose"; import { CouponRepo, CouponUsageRepo } from "./coupon.repository"; import { CreateCouponDTO } from "./DTO/createCoupon.dto"; import { UpdateCouponDTO } from "./DTO/updateCoupon.dto"; import { ValidateCouponDTO } from "./DTO/validateCoupon.dto"; import { CartMessage, CommonMessage, CouponMessage, ShopMessage } from "../../common/enums/message.enum"; import { BadRequestError } from "../../core/app/app.errors"; import { IOCTYPES } from "../../IOC/ioc.types"; import { TimeService } from "../../utils/time.service"; import { CartRepository, CartShipItemRepo } from "../cart/cart.repository"; import { CartService } from "../cart/cart.service"; import { ICartShipmentItem } from "../cart/models/Abstraction/ICartShipmentItem"; import { PaymentService } from "../payment/providers/payment.service"; import { OwnerRef } from "../shop/models/Abstraction/IShop"; import { ShopRepo } from "../shop/shop.repository"; @injectable() class CouponService { @inject(IOCTYPES.CouponRepo) couponRepo: CouponRepo; @inject(IOCTYPES.CouponUsageRepo) couponUsageRepo: CouponUsageRepo; @inject(IOCTYPES.PaymentService) paymentService: PaymentService; @inject(IOCTYPES.CartRepository) cartRepo: CartRepository; @inject(IOCTYPES.CartService) cartService: CartService; @inject(IOCTYPES.ShopRepo) shopRepo: ShopRepo; @inject(IOCTYPES.CartShipItemRepo) cartShipItemRepo: CartShipItemRepo; async getSellersCoupons() { const coupons = await this.couponRepo.model.aggregate([ { $match: { deleted: false, }, }, { $lookup: { from: "shops", localField: "shopId", foreignField: "_id", as: "shop", }, }, { $unwind: "$shop", }, { $match: { "shop.ownerRef": OwnerRef.SELLER, "shop.deleted": false, }, }, { $project: { shop: 0, }, }, ]); return { coupons }; } async getSellerCoupons(ownerId: string, ownerRef: OwnerRef) { const shop = await this.shopRepo.model.findOne({ owner: ownerId, ownerRef }); if (!shop) throw new BadRequestError(ShopMessage.ShopNotFound); return { coupons: await this.couponRepo.model.find({ shopId: shop._id, deleted: false }), }; } async getSingleCoupon(couponId: string) { if (!isValidObjectId(couponId)) throw new BadRequestError(CouponMessage.InvalidId); const coupon = await this.couponRepo.model.findById(couponId); if (!coupon) throw new BadRequestError(CouponMessage.InvalidId); return { coupon }; } //############################## async createCoupon(createDto: CreateCouponDTO, ownerId: string, ownerRef: OwnerRef) { const { discountAmount, discountPercentage, includedProducts, excludedProducts } = createDto; if (!discountAmount && !discountPercentage) throw new BadRequestError(CouponMessage.DiscountAmountOrPercentageRequired); if (discountAmount && discountPercentage) throw new BadRequestError(CouponMessage.BothDiscountOrPercentageShouldNotSend); if (includedProducts && excludedProducts) throw new BadRequestError(CouponMessage.BothIncludedAndExcludedProductsShouldNotSend); const shop = await this.shopRepo.model.findOne({ owner: ownerId, ownerRef }); if (!shop) throw new BadRequestError(ShopMessage.ShopNotFound); const code = this.generateCouponCode(); const coupon = await this.couponRepo.model.create({ shopId: shop._id, code, ...createDto, }); return { coupon }; } //############################## async updateCoupon(updateDto: UpdateCouponDTO, ownerId: string, couponId: string, ownerRef: OwnerRef) { if (!isValidObjectId(couponId)) throw new BadRequestError(CouponMessage.InvalidId); const shop = await this.shopRepo.model.findOne({ owner: ownerId, ownerRef }); if (!shop) throw new BadRequestError(ShopMessage.ShopNotFound); const { discountAmount, discountPercentage, includedProducts, excludedProducts } = updateDto; if (!discountAmount && !discountPercentage) throw new BadRequestError(CouponMessage.DiscountAmountOrPercentageRequired); if (discountAmount && discountPercentage) throw new BadRequestError(CouponMessage.BothDiscountOrPercentageShouldNotSend); if (includedProducts && excludedProducts) throw new BadRequestError(CouponMessage.BothIncludedAndExcludedProductsShouldNotSend); const updatedCoupon = await this.couponRepo.model.findOneAndUpdate( { _id: couponId, shopId: shop._id }, { ...updateDto }, { new: true }, ); if (!updatedCoupon) throw new BadRequestError(CouponMessage.InvalidId); return { message: CommonMessage.Updated, coupon: updatedCoupon, }; } //############################## async changeCouponStatus(ownerId: string, couponId: string, ownerRef: OwnerRef) { if (!isValidObjectId(couponId)) throw new BadRequestError(CouponMessage.InvalidId); const shop = await this.shopRepo.model.findOne({ owner: ownerId, ownerRef }); if (!shop) throw new BadRequestError(ShopMessage.ShopNotFound); const coupon = await this.couponRepo.model.findOne({ _id: couponId, shopId: shop._id }); if (!coupon) throw new BadRequestError(CouponMessage.InvalidId); const updatedCoupon = await this.couponRepo.model.findOneAndUpdate( { _id: couponId, shopId: shop._id }, { isActive: !coupon.isActive }, { new: true }, ); return { message: CommonMessage.Updated, coupon: updatedCoupon, }; } //############################## async applyCouponDiscount(validateDto: ValidateCouponDTO, userId: string) { const session = await startSession(); session.startTransaction(); try { const { price, cartShipmentItems } = await this.paymentService.getPaymentInfoS(userId); const userCart = await this.cartRepo.model.findOne({ user: userId }).session(session); if (!userCart) throw new BadRequestError(CartMessage.CartNotFound); // if (cart.coupon_discount) throw new BadRequestError(CouponMessage.CouponAlreadyInCart); const cartShipmentItem = cartShipmentItems.find((item) => item._id.toString() === validateDto.cartShipmentItemId); if (!cartShipmentItem) throw new BadRequestError(CartMessage.InvalidCartShipmentItem); const validCartShipmentItem = await this.cartShipItemRepo.model.findById(cartShipmentItem._id).session(session); if (!validCartShipmentItem) throw new BadRequestError(CartMessage.InvalidCartShipmentItem); if (cartShipmentItem.totalCouponDiscount) throw new BadRequestError(CouponMessage.CouponAlreadyInCart); const { total_payable_price } = price; const { coupon } = await this.validateCoupon(validateDto.code, cartShipmentItem.totalPaymentPrice, userId, session); let discount = 0; if (coupon.discountAmount) { discount = coupon.discountAmount; } else if (coupon.discountPercentage) { discount = (total_payable_price * coupon.discountPercentage) / 100; } if (coupon.category && coupon.category.length > 0) { await this.checkCartItemCategory( userId, coupon.category.map((cat) => cat._id.toString()), ); } if (coupon.includedProducts && coupon.includedProducts.length > 0) { await this.checkCartItemIncludedProducts(cartShipmentItem, coupon.includedProducts); } if (coupon.excludedProducts && coupon.excludedProducts.length > 0) { await this.checkCartItemExcludedProducts(cartShipmentItem, coupon.excludedProducts); } await this.couponUsageRepo.model.findOneAndUpdate( { user: userId, coupon: coupon._id }, { $inc: { usageCount: 1 } }, { new: true, upsert: true, session }, ); cartShipmentItem.totalCouponDiscount = discount; cartShipmentItem.totalPaymentPrice = cartShipmentItem.totalPaymentPrice - discount; userCart.coupon_discount = discount; userCart.total_discount += discount; coupon.usageCount += 1; await validCartShipmentItem.save({ session }); await userCart.save({ session }); await coupon.save({ session }); await session.commitTransaction(); await session.endSession(); return { message: CouponMessage.CouponAdded, afterDiscount: total_payable_price - discount }; } catch (error) { await session.abortTransaction(); await session.endSession(); throw error; } } //================================> helper method private generateCouponCode(length = 8) { return randomBytes(length).toString("hex").slice(0, length).toUpperCase(); } async checkCartItemExcludedProducts(shipmentItem: ICartShipmentItem, excludedProducts: number[]) { const items = shipmentItem.shipmentItems.filter((item) => excludedProducts.includes(item.product)); console.log({ items }); if (items.length > 0) throw new BadRequestError(CouponMessage.InvalidProduct); return true; } async checkCartItemIncludedProducts(shipmentItem: ICartShipmentItem, includedProducts: number[]) { const items = shipmentItem.shipmentItems.filter((item) => includedProducts.includes(item.product)); if (!items.length) throw new BadRequestError(CouponMessage.InvalidProduct); return true; } async checkCartItemCategory(userId: string, category: string[]) { const { cart } = await this.cartService.getCartS(userId); const items = cart.items.filter((item) => category.includes(item.product.category._id)); if (items.length === 0) throw new BadRequestError(CouponMessage.InvalidCategory); return true; } async removeCouponByAdmin(couponId: string) { const deletedCoupon = await this.couponRepo.model.findByIdAndUpdate(couponId, { deleted: true }, { new: true }); if (!deletedCoupon) throw new BadRequestError(CouponMessage.NotFound); return { message: CommonMessage.Deleted }; } //############################## private async validateCoupon(couponCode: string, totalPrice: number, userId: string, session: ClientSession) { const coupon = await this.couponRepo.model.findOne({ code: couponCode, isActive: true, deleted: false }).session(session); if (!coupon) throw new BadRequestError(CouponMessage.NotFound); const expired = TimeService.isPersianDateExpired(coupon.expirationDate); if (expired) throw new BadRequestError(CouponMessage.Expired); if (coupon.usageLimit && coupon.usageCount >= coupon.usageLimit) throw new BadRequestError(CouponMessage.UsageLimit); const userUsage = await this.couponUsageRepo.model.findOne({ coupon: coupon._id, user: userId }).session(session); if (userUsage && userUsage.usageCount >= coupon.userUsageLimit) throw new BadRequestError(CouponMessage.UserCouponUsageLimit); if (coupon.minPurchaseAmount && totalPrice < coupon.minPurchaseAmount) { throw new BadRequestError([ CouponMessage.AmountIsNotSatisfied, `حداقل مبلغ سبد خرید برای استفاده از این کد تخفیف ${coupon.minPurchaseAmount}`, ]); } return { coupon }; } } export { CouponService };