Files
shop-api/src/modules/Coupon/coupon.service.ts
T
morteza-mortezai f0ccb5e025 fix : total discount
2025-10-28 15:16:29 +03:30

289 lines
11 KiB
TypeScript

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 };