first
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
import { Expose } from "class-transformer";
|
||||
import { IsArray, IsBoolean, IsMongoId, IsNotEmpty, IsNumber, IsOptional, IsString } from "class-validator";
|
||||
|
||||
import { ApiProperty } from "../../../common/decorator/swggerDocs";
|
||||
import { IsValidPersianDate } from "../../../common/decorator/validation.decorator";
|
||||
|
||||
export class CreateCouponDTO {
|
||||
@Expose()
|
||||
@IsOptional()
|
||||
@IsNotEmpty()
|
||||
@IsNumber()
|
||||
@ApiProperty({ type: "number", example: 10, description: "Discount percentage for the coupon" })
|
||||
discountPercentage?: number;
|
||||
|
||||
@Expose()
|
||||
@IsOptional()
|
||||
@IsNotEmpty()
|
||||
@IsNumber()
|
||||
@ApiProperty({ type: "number", example: 10000, description: "Flat discount amount" })
|
||||
discountAmount?: number;
|
||||
|
||||
@Expose()
|
||||
@IsOptional()
|
||||
@IsNotEmpty()
|
||||
@IsNumber()
|
||||
@ApiProperty({ type: "number", example: 5, description: "Maximum number of uses for the coupon" })
|
||||
usageLimit: number;
|
||||
|
||||
@Expose()
|
||||
@IsOptional()
|
||||
@IsNotEmpty()
|
||||
@IsNumber()
|
||||
@ApiProperty({ type: "number", example: 5, description: "Maximum number of uses for the coupon per user" })
|
||||
userUsageLimit: number;
|
||||
|
||||
@Expose()
|
||||
@IsOptional()
|
||||
@IsNotEmpty()
|
||||
@IsNumber()
|
||||
@ApiProperty({ type: "number", example: 100000, description: "Minimum purchase amount to use the coupon in toman" })
|
||||
minPurchaseAmount: number;
|
||||
|
||||
@Expose()
|
||||
@IsOptional()
|
||||
@IsNotEmpty()
|
||||
@IsArray()
|
||||
@IsMongoId({ each: true })
|
||||
@ApiProperty({ type: "Array", example: ["60d21b4967d0d8992e610c85"], description: "categories for the coupon" })
|
||||
category?: string[];
|
||||
|
||||
@Expose()
|
||||
@IsOptional()
|
||||
@IsNotEmpty()
|
||||
@IsArray()
|
||||
@ApiProperty({ type: "Array", example: [100001, 100102], description: "List of included product IDs" })
|
||||
includedProducts?: number[];
|
||||
|
||||
@Expose()
|
||||
@IsOptional()
|
||||
@IsNotEmpty()
|
||||
@IsArray()
|
||||
@ApiProperty({ type: "Array", example: [100001, 100102], description: "List of excluded product IDs" })
|
||||
excludedProducts?: number[];
|
||||
|
||||
@Expose()
|
||||
@IsOptional()
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
@ApiProperty({ type: "string", example: "کد تخفیف", description: "Description of the coupon" })
|
||||
description: string;
|
||||
|
||||
@Expose()
|
||||
@IsOptional()
|
||||
@IsNotEmpty()
|
||||
@IsBoolean()
|
||||
@ApiProperty({ type: "boolean", example: false, description: "Whether the coupon applies to special sales" })
|
||||
includeSpecialSale: boolean;
|
||||
|
||||
@Expose()
|
||||
@IsNotEmpty()
|
||||
@IsValidPersianDate()
|
||||
@ApiProperty({ type: "string", example: "1401/06/05", description: "Coupon expiration date in jalali format" })
|
||||
expirationDate: string;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { Expose } from "class-transformer";
|
||||
import { IsArray, IsMongoId, IsNotEmpty, IsNumber, IsOptional } from "class-validator";
|
||||
|
||||
import { ApiProperty } from "../../../common/decorator/swggerDocs";
|
||||
|
||||
export class UpdateCouponDTO {
|
||||
@Expose()
|
||||
@IsOptional()
|
||||
@IsNotEmpty()
|
||||
@IsNumber()
|
||||
@ApiProperty({ type: "number", example: 10, description: "Discount percentage for the coupon" })
|
||||
discountPercentage?: number;
|
||||
|
||||
@Expose()
|
||||
@IsOptional()
|
||||
@IsNotEmpty()
|
||||
@IsNumber()
|
||||
@ApiProperty({ type: "number", example: 10000, description: "Flat discount amount" })
|
||||
discountAmount?: number;
|
||||
|
||||
@Expose()
|
||||
@IsOptional()
|
||||
@IsNotEmpty()
|
||||
@IsNumber()
|
||||
@ApiProperty({ type: "number", example: 5, description: "Maximum number of uses for the coupon" })
|
||||
usageLimit?: number;
|
||||
|
||||
@Expose()
|
||||
@IsOptional()
|
||||
@IsNotEmpty()
|
||||
@IsNumber()
|
||||
@ApiProperty({ type: "number", example: 5, description: "Maximum number of uses for the coupon per user" })
|
||||
userUsageLimit?: number;
|
||||
|
||||
@Expose()
|
||||
@IsOptional()
|
||||
@IsNotEmpty()
|
||||
@IsNumber()
|
||||
@ApiProperty({ type: "number", example: 100000, description: "Minimum purchase amount to use the coupon in toman" })
|
||||
minPurchaseAmount?: number;
|
||||
|
||||
@Expose()
|
||||
@IsOptional()
|
||||
@IsNotEmpty()
|
||||
@IsArray()
|
||||
@IsMongoId({ each: true })
|
||||
@ApiProperty({ type: "Array", example: ["60d21b4967d0d8992e610c85"], description: "categories for the coupon" })
|
||||
category?: string[];
|
||||
|
||||
@Expose()
|
||||
@IsOptional()
|
||||
@IsNotEmpty()
|
||||
@IsArray()
|
||||
@ApiProperty({ type: "Array", example: [100001, 100102], description: "List of included product IDs" })
|
||||
includedProducts?: number[];
|
||||
|
||||
@Expose()
|
||||
@IsOptional()
|
||||
@IsNotEmpty()
|
||||
@IsArray()
|
||||
@ApiProperty({ type: "Array", example: [100001, 100102], description: "List of excluded product IDs" })
|
||||
excludedProducts?: number[];
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Expose } from "class-transformer";
|
||||
import { IsNotEmpty, IsString } from "class-validator";
|
||||
|
||||
import { ApiProperty } from "../../../common/decorator/swggerDocs";
|
||||
import { IsValidId } from "../../../common/decorator/validation.decorator";
|
||||
import { CartShipmentItemModel } from "../../cart/models/cartShipmentItem.model";
|
||||
|
||||
export class ValidateCouponDTO {
|
||||
@Expose()
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
@ApiProperty({ type: "string", example: "DISCOUNT2024" })
|
||||
code: string;
|
||||
|
||||
@Expose()
|
||||
@IsNotEmpty()
|
||||
@IsValidId(CartShipmentItemModel)
|
||||
@ApiProperty({ type: "string", example: "5f7b1b1b1b1b1b1b1b1b1b1b1", description: "cartShipment item id" })
|
||||
cartShipmentItemId: string;
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { Request } from "express";
|
||||
import rateLimit from "express-rate-limit";
|
||||
import { inject } from "inversify";
|
||||
import { controller, httpGet, httpPatch, httpPost, request, requestBody, requestParam } from "inversify-express-utils";
|
||||
|
||||
import { CouponService } from "./coupon.service";
|
||||
import { HttpStatus } from "../../common";
|
||||
import { CreateCouponDTO } from "./DTO/createCoupon.dto";
|
||||
import { UpdateCouponDTO } from "./DTO/updateCoupon.dto";
|
||||
import { ValidateCouponDTO } from "./DTO/validateCoupon.dto";
|
||||
import { BaseController } from "../../common/base/controller";
|
||||
import { ApiAuth, ApiModel, ApiOperation, ApiParam, ApiResponse, ApiTags } from "../../common/decorator/swggerDocs";
|
||||
import { appConfig } from "../../core/config/app.config";
|
||||
import { Guard } from "../../core/middlewares/guard.middleware";
|
||||
import { ValidationMiddleware } from "../../core/middlewares/validator.middleware";
|
||||
import { IOCTYPES } from "../../IOC/ioc.types";
|
||||
import { ISeller } from "../seller/models/Abstraction/ISeller";
|
||||
import { OwnerRef } from "../shop/models/Abstraction/IShop";
|
||||
import { IUser } from "../user/models/Abstraction/IUser";
|
||||
|
||||
@controller("/coupon")
|
||||
@ApiTags("Coupon")
|
||||
class CouponController extends BaseController {
|
||||
@inject(IOCTYPES.CouponService) couponService: CouponService;
|
||||
|
||||
@ApiOperation("get all seller created coupon ==> login as seller")
|
||||
@ApiResponse("successful")
|
||||
@ApiAuth()
|
||||
@httpGet("", Guard.authSeller())
|
||||
public async getSellerCoupons(@request() req: Request) {
|
||||
const seller = req.user as ISeller;
|
||||
const data = await this.couponService.getSellerCoupons(seller._id.toString(), OwnerRef.SELLER);
|
||||
return this.response(data);
|
||||
}
|
||||
|
||||
@ApiOperation("get a single coupon")
|
||||
@ApiParam("id", "coupon id", true)
|
||||
@ApiAuth()
|
||||
@httpGet("/:id", Guard.authSeller())
|
||||
public async getSingleCoupon(@requestParam("id") couponId: string) {
|
||||
const data = await this.couponService.getSingleCoupon(couponId);
|
||||
return this.response(data);
|
||||
}
|
||||
|
||||
@ApiOperation("create a Coupon ==> login as seller")
|
||||
@ApiResponse("successful", HttpStatus.Created)
|
||||
@ApiModel(CreateCouponDTO)
|
||||
@ApiAuth()
|
||||
@httpPost("", rateLimit(appConfig.rate), Guard.authSeller(), ValidationMiddleware.validateInput(CreateCouponDTO))
|
||||
public async createCoupon(@requestBody() createDto: CreateCouponDTO, @request() req: Request) {
|
||||
const seller = req.user as ISeller;
|
||||
const data = await this.couponService.createCoupon(createDto, seller._id.toString(), OwnerRef.SELLER);
|
||||
return this.response(data, HttpStatus.Created);
|
||||
}
|
||||
|
||||
@ApiOperation("deactivate a coupon ==> login as seller")
|
||||
@ApiResponse("successful")
|
||||
@ApiAuth()
|
||||
@ApiParam("id", "coupon id", true)
|
||||
@httpPatch("/:id/status", Guard.authSeller())
|
||||
public async changeCouponStatus(@request() req: Request, @requestParam("id") couponId: string) {
|
||||
const seller = req.user as ISeller;
|
||||
const data = await this.couponService.changeCouponStatus(seller._id.toString(), couponId, OwnerRef.SELLER);
|
||||
return this.response(data);
|
||||
}
|
||||
|
||||
@ApiOperation("update a seller coupon ==> login as seller")
|
||||
@ApiResponse("successful")
|
||||
@ApiModel(UpdateCouponDTO)
|
||||
@ApiParam("id", "coupon id", true)
|
||||
@ApiAuth()
|
||||
@httpPatch("/:id", Guard.authSeller(), ValidationMiddleware.validateInput(UpdateCouponDTO))
|
||||
public async updateSellerCoupon(
|
||||
@requestBody() updateDto: UpdateCouponDTO,
|
||||
@request() req: Request,
|
||||
@requestParam("id") couponId: string,
|
||||
) {
|
||||
const seller = req.user as ISeller;
|
||||
const data = await this.couponService.updateCoupon(updateDto, seller._id.toString(), couponId, OwnerRef.SELLER);
|
||||
return this.response(data);
|
||||
}
|
||||
|
||||
@ApiOperation("add a coupon to user cart ===> login as user")
|
||||
@ApiResponse("successful")
|
||||
@ApiResponse("bad request", HttpStatus.BadRequest)
|
||||
@ApiModel(ValidateCouponDTO)
|
||||
@ApiAuth()
|
||||
@httpPost("/add-coupon", Guard.authUser(), ValidationMiddleware.validateInput(ValidateCouponDTO))
|
||||
public async addCoupon(@requestBody() validateDto: ValidateCouponDTO, @request() req: Request) {
|
||||
const user = req.user as IUser;
|
||||
const data = await this.couponService.applyCouponDiscount(validateDto, user._id.toString());
|
||||
return this.response(data);
|
||||
}
|
||||
}
|
||||
|
||||
export { CouponController };
|
||||
@@ -0,0 +1,24 @@
|
||||
import { ICoupon } from "./models/Abstraction/ICoupon";
|
||||
import { ICouponUsage } from "./models/Abstraction/ICouponUsage";
|
||||
import { CouponModel } from "./models/coupon.model";
|
||||
import { CouponUsageModel } from "./models/couponUsage.model";
|
||||
import { BaseRepository } from "../../common/base/repository";
|
||||
|
||||
export class CouponRepo extends BaseRepository<ICoupon> {
|
||||
constructor() {
|
||||
super(CouponModel);
|
||||
}
|
||||
}
|
||||
export function createCouponRepo(): CouponRepo {
|
||||
return new CouponRepo();
|
||||
}
|
||||
|
||||
export class CouponUsageRepo extends BaseRepository<ICouponUsage> {
|
||||
constructor() {
|
||||
super(CouponUsageModel);
|
||||
}
|
||||
}
|
||||
|
||||
export function createCouponUsageRepo(): CouponUsageRepo {
|
||||
return new CouponUsageRepo();
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
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;
|
||||
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 };
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Types } from "mongoose";
|
||||
|
||||
export interface ICoupon {
|
||||
shopId: Types.ObjectId;
|
||||
discountAmount: number;
|
||||
discountPercentage: number;
|
||||
usageLimit: number;
|
||||
usageCount: number;
|
||||
userUsageLimit: number;
|
||||
code: string;
|
||||
minPurchaseAmount: number;
|
||||
category: Types.ObjectId[];
|
||||
includedProducts: number[];
|
||||
excludedProducts: number[];
|
||||
description: string;
|
||||
isActive: boolean;
|
||||
includeSpecialSale: boolean;
|
||||
expirationDate: string;
|
||||
deleted: boolean;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Types } from "mongoose";
|
||||
|
||||
export interface ICouponUsage {
|
||||
coupon: Types.ObjectId;
|
||||
user: Types.ObjectId;
|
||||
usageCount: number;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Schema, model } from "mongoose";
|
||||
|
||||
import { ICoupon } from "./Abstraction/ICoupon";
|
||||
|
||||
const CouponSchema = new Schema<ICoupon>(
|
||||
{
|
||||
shopId: { type: Schema.Types.ObjectId, ref: "Shop", required: true },
|
||||
discountPercentage: { type: Number, default: 0 },
|
||||
discountAmount: { type: Number, default: 0 },
|
||||
usageLimit: { type: Number, default: null },
|
||||
usageCount: { type: Number, default: 0 },
|
||||
userUsageLimit: { type: Number, default: null },
|
||||
code: { type: String, required: true, unique: true },
|
||||
minPurchaseAmount: { type: Number, default: null },
|
||||
category: { type: [Schema.Types.ObjectId], ref: "Category", default: [] },
|
||||
includedProducts: { type: [Number], ref: "Product", default: [] },
|
||||
excludedProducts: { type: [Number], ref: "Product", default: [] },
|
||||
description: { type: String, default: null },
|
||||
isActive: { type: Boolean, default: true },
|
||||
includeSpecialSale: { type: Boolean, default: true },
|
||||
expirationDate: { type: String, required: true },
|
||||
deleted: { type: Boolean, default: false },
|
||||
},
|
||||
{ timestamps: true, toJSON: { versionKey: false }, id: false },
|
||||
);
|
||||
|
||||
const CouponModel = model<ICoupon>("Coupon", CouponSchema);
|
||||
|
||||
export { CouponModel };
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Schema, model } from "mongoose";
|
||||
|
||||
import { ICouponUsage } from "./Abstraction/ICouponUsage";
|
||||
|
||||
const CouponUsageSchema = new Schema<ICouponUsage>(
|
||||
{
|
||||
coupon: { type: Schema.Types.ObjectId, ref: "Coupon", required: true },
|
||||
user: { type: Schema.Types.ObjectId, ref: "User", required: true },
|
||||
usageCount: { type: Number, required: true },
|
||||
},
|
||||
{ timestamps: true, toJSON: { versionKey: false }, id: false },
|
||||
);
|
||||
|
||||
CouponUsageSchema.index({ coupon: 1, user: 1 }, { unique: true });
|
||||
const CouponUsageModel = model<ICouponUsage>("CouponUsage", CouponUsageSchema);
|
||||
export { CouponUsageModel };
|
||||
Reference in New Issue
Block a user