This commit is contained in:
mahyargdz
2025-09-14 15:15:03 +03:30
commit be82059172
534 changed files with 51310 additions and 0 deletions
@@ -0,0 +1,66 @@
import { Expose, Type } from "class-transformer";
import { ArrayMinSize, IsArray, IsInt, IsNotEmpty, IsOptional, IsString, ValidateNested } from "class-validator";
import { ApiProperty } from "../../../common/decorator/swggerDocs";
import { IsValidId, IsValidPersianDate } from "../../../common/decorator/validation.decorator";
import { OrderModel } from "../models/order.model";
export class InsertTrackCodeDTO {
@Expose()
@IsNotEmpty()
@IsString()
@ApiProperty({ type: "string", description: "the track code of order", example: "1234d3656456" })
trackCode: string;
@Expose()
@IsOptional()
@IsNotEmpty()
@IsString()
@IsValidPersianDate()
@ApiProperty({ type: "string", description: "the posting date of order", example: "1403/08/09" })
postingDate: string;
@Expose()
@IsOptional()
@IsNotEmpty()
@IsString()
@ApiProperty({ type: "string", description: "the url of postingReceipt of order", example: "https://images.com" })
postingReceipt?: string;
}
export class TrackCodesDTO extends InsertTrackCodeDTO {
@Expose()
@IsNotEmpty()
@IsInt()
@IsValidId(OrderModel)
@ApiProperty({ type: "string", description: "the order id", example: 10102 })
orderId: number;
}
export class InsertTrackCodeGroupDTO {
@Expose()
@IsNotEmpty()
@IsArray()
@ArrayMinSize(1)
@ValidateNested({ each: true })
@Type(() => TrackCodesDTO)
@ApiProperty({
type: "Array",
description: "the track codes of order",
example: [
{
trackCode: "1234d3656456",
postingDate: "1403/08/09",
postingReceipt: "https://images.com ==> it is optional",
orderId: 10102,
},
{
trackCode: "1234d365356",
postingDate: "1403/08/09",
postingReceipt: "https://images.com ==> it is optional",
orderId: 10104,
},
],
})
trackCodes: TrackCodesDTO[];
}
+84
View File
@@ -0,0 +1,84 @@
import { Expose, Type } from "class-transformer";
import { ProductDTO, ProductDetailVariantDTO } from "../../product/DTO/product.dto";
import { ShipmentMethodDTO } from "../../shipment/DTO/shipment.dto";
import { ShopDTO } from "../../shop/DTO/shop.dto";
export class ShipmentItemsDTO {
@Expose()
_id: string;
@Expose()
product: ProductDTO;
@Expose()
variant: ProductDetailVariantDTO;
@Expose()
quantity: number;
@Expose()
cancelled_quantity: number;
@Expose()
returned_quantity: number;
@Expose()
selling_price: number;
@Expose()
retail_price: number;
@Expose()
discount_percent: number;
@Expose()
shipmentCost: number;
}
export class OrderItemDTO {
@Expose()
_id: string;
@Expose()
order_id: number;
@Expose()
@Type(() => ShopDTO)
shop: ShopDTO;
@Expose()
@Type(() => ShipmentItemsDTO)
shipmentItems: ShipmentItemsDTO[];
@Expose()
@Type(() => ShipmentMethodDTO)
shipper: ShipmentMethodDTO;
@Expose()
trackCode: string;
@Expose()
itemsCount: number;
@Expose()
totalPaymentPrice: number;
@Expose()
totalRetailPrice: number;
@Expose()
totalSellingPrice: number;
@Expose()
totalShipmentCost: number;
@Expose()
postingDate: string;
@Expose()
postingReceipt: string;
@Expose()
status: string;
}
+21
View File
@@ -0,0 +1,21 @@
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 { OrderItemModel } from "../models/orderItem.model";
export class ReceivedOrderItemDTO {
@Expose()
@IsNotEmpty()
@IsValidId(OrderItemModel)
@IsString()
@ApiProperty({ type: "string", description: "the id of orderItem ", example: "66f7bf9a71d13496054d7c2a" })
orderItemId: string;
// @Expose()
// @IsNotEmpty()
// @IsString()
// @ApiProperty({ type: "string", description: "the id of shipment item ", example: "66f7bf9a71d13496054d7c2a" })
// shipmentItemId: string;
}
+61
View File
@@ -0,0 +1,61 @@
import { Expose, Transform, Type, plainToClass } from "class-transformer";
import { OrderItemDTO } from "./order.dto";
import { ShipmentAddressDTO } from "./userOrder.dto";
import { OrdersStatus } from "../../../common/enums/order.enum";
import { TimeService } from "../../../utils/time.service";
import { PaymentDTO } from "../../payment/DTO/payment.dto";
import { MiniUserDTO } from "../../user/DTO/user.dto";
import { IOrder } from "../models/Abstraction/IOrder";
export class SellerOrdersDTO {
@Expose()
_id: number;
@Expose()
@Type(() => MiniUserDTO)
user: MiniUserDTO;
@Expose()
@Type(() => PaymentDTO)
payment: PaymentDTO;
@Expose()
@Type(() => OrderItemDTO)
orderItems: OrderItemDTO;
@Expose()
@Type(() => ShipmentAddressDTO)
shipmentAddress: ShipmentAddressDTO;
@Expose()
orderStatus: OrdersStatus;
@Expose()
totalSellingPrice: number;
@Expose()
totalRetailPrice: number;
@Expose()
totalShipmentCost: number;
@Expose()
totalPaymentPrice: number;
@Expose()
itemsCount: number;
@Expose()
@Transform(({ value }) => TimeService.convertGregorianToPersian(new Date(value), true))
createdAt: string;
public static transformOrder(data: IOrder): SellerOrdersDTO {
const OrderDTO = plainToClass(SellerOrdersDTO, data, {
excludeExtraneousValues: true,
enableImplicitConversion: true,
});
return OrderDTO;
}
}
+65
View File
@@ -0,0 +1,65 @@
import { Expose, Transform, Type, plainToClass } from "class-transformer";
import { OrderItemDTO } from "./order.dto";
import { OrdersStatus } from "../../../common/enums/order.enum";
import { TimeService } from "../../../utils/time.service";
import { PaymentDTO } from "../../payment/DTO/payment.dto";
import { UserDTO } from "../../user/DTO/user.dto";
import { IOrder } from "../models/Abstraction/IOrder";
export class ShipmentAddressDTO {
@Expose()
address: string;
@Expose()
city: string;
@Expose()
province: string;
@Expose()
plaque: string;
@Expose()
postalCode: string;
@Expose()
phone: string;
}
export class UserOrderDTO {
@Expose()
_id: number;
@Expose()
@Type(() => UserDTO)
user: UserDTO;
@Expose()
@Type(() => PaymentDTO)
payment: PaymentDTO;
@Expose()
@Type(() => OrderItemDTO)
orderItems: OrderItemDTO[];
@Expose()
@Type(() => ShipmentAddressDTO)
shipmentAddress: ShipmentAddressDTO;
@Expose()
orderStatus: OrdersStatus;
@Expose()
@Transform(({ value }) => TimeService.convertGregorianToPersian(new Date(value), true))
createdAt: string;
public static transformOrder(data: IOrder): UserOrderDTO {
const OrderDTO = plainToClass(UserOrderDTO, data, {
excludeExtraneousValues: true,
enableImplicitConversion: true,
});
return OrderDTO;
}
}
@@ -0,0 +1,20 @@
import { Types } from "mongoose";
import { OrdersStatus } from "../../../../common/enums/order.enum";
export interface IOrder {
_id: number;
user: Types.ObjectId;
payment: Types.ObjectId;
shipmentAddress: IShipmentAddress;
orderStatus: OrdersStatus;
}
export interface IShipmentAddress {
address: string;
city: string;
province: string;
plaque: string;
postalCode: string;
phone: string;
}
@@ -0,0 +1,22 @@
import { Types } from "mongoose";
import { OrderItemsStatus } from "../../../../common/enums/order.enum";
import { IShipmentItems } from "../../../cart/models/Abstraction/ICartShipmentItem";
export interface IOrderItem {
_id?: Types.ObjectId;
order: number;
shop: Types.ObjectId;
shipmentItems: IShipmentItems[];
shipper: number;
totalSellingPrice: number;
totalRetailPrice: number;
totalShipmentCost: number;
totalCouponDiscount: number;
totalPaymentPrice: number;
itemsCount: number;
trackCode?: string;
postingDate?: string;
postingReceipt?: string;
status: OrderItemsStatus;
}
+48
View File
@@ -0,0 +1,48 @@
import mongoose, { Schema, model } from "mongoose";
import { IOrder, IShipmentAddress } from "./Abstraction/IOrder";
import { OrdersStatus } from "../../../common/enums/order.enum";
// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports
const AutoIncrement = require("mongoose-sequence")(mongoose);
const ShipmentAddressSchema = new Schema<IShipmentAddress>({
address: { type: String, required: true },
city: { type: String, required: true },
province: { type: String, required: true },
phone: { type: String, required: true },
plaque: { type: String, required: true },
postalCode: { type: String, required: true },
});
const OrderSchema = new Schema<IOrder>(
{
_id: Number,
user: { type: Schema.Types.ObjectId, ref: "User", required: true, index: true },
payment: { type: Schema.Types.ObjectId, ref: "CartPayment", required: true, index: true },
shipmentAddress: { type: ShipmentAddressSchema, required: true },
orderStatus: { type: String, enum: OrdersStatus, default: OrdersStatus.wait_payment },
},
{ timestamps: true, toJSON: { versionKey: false, virtuals: true }, _id: false, id: false },
);
OrderSchema.virtual("orderItems", {
ref: "OrderItem",
localField: "_id",
foreignField: "order",
justOne: false,
});
OrderSchema.pre(["find", "findOne"], function (next) {
this.populate([
{ path: "user" },
{ path: "payment" },
{ path: "orderItems", populate: { path: "shop shipper shipmentItems.product shipmentItems.variant" } },
]);
next();
});
OrderSchema.plugin(AutoIncrement, { id: "order_id", inc_field: "_id", start_seq: 10100 });
const OrderModel = model<IOrder>("Order", OrderSchema);
export { OrderModel };
@@ -0,0 +1,34 @@
import { Schema, model } from "mongoose";
import { IOrderItem } from "./Abstraction/IOrderItem";
import { OrderItemsStatus } from "../../../common/enums/order.enum";
import { OrderQueue } from "../../../queues/order/OrderQueue";
import { ShipmentItemsSchema } from "../../cart/models/cartShipmentItem.model";
const OrderItemSchema = new Schema<IOrderItem>(
{
order: { type: Number, ref: "Order", required: true },
shop: { type: Schema.Types.ObjectId, ref: "Shop", required: true, index: true },
shipmentItems: { type: [ShipmentItemsSchema], default: [] },
shipper: { type: Number, ref: "Shipment", required: true },
totalSellingPrice: { type: Number, required: true },
totalRetailPrice: { type: Number, required: true },
totalShipmentCost: { type: Number, required: true },
totalCouponDiscount: { type: Number, default: 0 },
totalPaymentPrice: { type: Number, required: true },
itemsCount: { type: Number, required: true },
trackCode: { type: String, default: null },
postingDate: { type: String, default: null },
postingReceipt: { type: String, default: null },
status: { type: String, enum: OrderItemsStatus, default: OrderItemsStatus.Processing },
},
{ timestamps: true, toJSON: { versionKey: false, virtuals: true }, id: false },
);
OrderItemSchema.post(["save", "findOneAndUpdate"], async function (doc) {
await OrderQueue.addOrderToCheckStatus(doc.order);
});
const OrderItemModel = model<IOrderItem>("OrderItem", OrderItemSchema);
export { OrderItemModel };
+164
View File
@@ -0,0 +1,164 @@
import { Request } from "express";
import rateLimit from "express-rate-limit";
import { inject } from "inversify";
import { controller, httpGet, httpPost, request, requestBody, requestParam } from "inversify-express-utils";
import { OrderService } from "./order.service";
import { HttpStatus } from "../../common";
import { InsertTrackCodeDTO, InsertTrackCodeGroupDTO } from "./DTO/insertTrackCode.dto";
import { ReceivedOrderItemDTO } from "./DTO/received.dto";
import { BaseController } from "../../common/base/controller";
import { ApiAuth, ApiFile, 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 { UploadService } from "../../utils/upload.service";
import { ISeller } from "../seller/models/Abstraction/ISeller";
import { IUser } from "../user/models/Abstraction/IUser";
@controller("/order")
@ApiTags("Order")
class OrderController extends BaseController {
@inject(IOCTYPES.OrderService) orderService: OrderService;
//###############################################################
//###############################################################
//###########################################################
//###########################################################
//getSingle order details
@ApiOperation("get an order details of seller")
@ApiResponse("successful", HttpStatus.Ok)
@ApiParam("orderId", "the user seller id", true)
@ApiAuth()
@httpGet("/:orderId/seller", Guard.authSeller())
public async getSellerOrderDetail(@requestParam("orderId") orderId: string, @request() req: Request) {
const seller = req.user as ISeller;
const data = await this.orderService.getSellerOrderDetail(+orderId, seller._id.toString());
return this.response(data);
}
@ApiOperation("get an order details of user")
@ApiResponse("successful", HttpStatus.Ok)
@ApiParam("orderId", "the user order id", true)
@ApiAuth()
@httpGet("/:orderId/user", Guard.authUser())
public async getUserOrderDetail(@requestParam("orderId") orderId: string, @request() req: Request) {
const user = req.user as IUser;
const data = await this.orderService.getUserOrderDetail(+orderId, user._id.toString());
return this.response(data);
}
@ApiOperation("get an order details of user")
@ApiResponse("successful", HttpStatus.Ok)
@ApiParam("orderId", "the user order id", true)
@ApiParam("orderItemId", "the user orderItem id", true)
@ApiAuth()
@httpGet("/:orderId/user/:orderItemId", Guard.authUser())
public async getUserOrderItemsDetail(
@requestParam("orderId") orderId: string,
@requestParam("orderItemId") orderItemId: string,
@request() req: Request,
) {
const user = req.user as IUser;
const data = await this.orderService.getUserOrderItemsDetail(+orderId, orderItemId, user._id.toString());
return this.response(data);
}
//###########################################################
//###########################################################
//insert track code
@ApiOperation("insert a track code in an orders of user")
@ApiResponse("successful", HttpStatus.Created)
@ApiModel(InsertTrackCodeGroupDTO)
@ApiAuth()
@httpPost("/seller/track-code/group", Guard.authSeller(), ValidationMiddleware.validateInput(InsertTrackCodeGroupDTO))
public async insertTrackCodeGroup(@request() req: Request, @requestBody() insertDto: InsertTrackCodeGroupDTO) {
const seller = req.user as ISeller;
const data = await this.orderService.insertTrackCodeGroup(seller._id.toString(), insertDto);
return this.response(data, HttpStatus.Created);
}
@ApiOperation("insert a track code in an order of seller")
@ApiResponse("successful", HttpStatus.Created)
@ApiModel(InsertTrackCodeDTO)
@ApiParam("orderId", "the seller order id", true)
@ApiAuth()
@httpPost("/:orderId/seller/track-code", Guard.authSeller(), ValidationMiddleware.validateInput(InsertTrackCodeDTO))
public async insertTrackCode(
@requestParam("orderId") orderId: string,
@request() req: Request,
@requestBody() insertDto: InsertTrackCodeDTO,
) {
const seller = req.user as ISeller;
const data = await this.orderService.insertTrackCode(+orderId, seller._id.toString(), insertDto);
return this.response(data, HttpStatus.Created);
}
@ApiOperation("insert a track code in an order of native shop by admin")
@ApiResponse("successful", HttpStatus.Created)
@ApiModel(InsertTrackCodeDTO)
@ApiParam("orderId", "the native shop order id", true)
@ApiAuth()
@httpPost("/:orderId/admin/track-code", Guard.authAdmin(), ValidationMiddleware.validateInput(InsertTrackCodeDTO))
public async insertNativeShopTrackCode(
@requestParam("orderId") orderId: string,
@request() req: Request,
@requestBody() insertDto: InsertTrackCodeDTO,
) {
const admin = req.user as IUser;
const data = await this.orderService.insertNativeShopTrackCode(+orderId, admin._id.toString(), insertDto);
return this.response(data, HttpStatus.Created);
}
@ApiOperation("confirm receiving an order items of seller ==> login as user")
@ApiResponse("successful", HttpStatus.Created)
@ApiModel(ReceivedOrderItemDTO)
@ApiParam("orderId", "the user order id", true)
@ApiAuth()
@httpPost("/:orderId/user/received", Guard.authUser(), ValidationMiddleware.validateInput(ReceivedOrderItemDTO))
public async setOrderItemReceived(
@requestParam("orderId") orderId: string,
@request() req: Request,
@requestBody() receivedOrderItemDTO: ReceivedOrderItemDTO,
) {
const user = req.user as IUser;
const data = await this.orderService.setOrderItemReceived(+orderId, user._id.toString(), receivedOrderItemDTO);
return this.response(data, HttpStatus.Created);
}
@ApiOperation("send the order status and tracking status to user")
@ApiResponse("successful", HttpStatus.Ok)
@ApiParam("orderId", "the user order id", true)
@ApiAuth()
@httpPost("/:orderId/user/tracking", rateLimit(appConfig.rate), Guard.authUser())
public async trackOrder(@request() req: Request, @requestParam("orderId") orderId: string) {
const user = req.user as IUser;
const data = await this.orderService.trackOrder(+orderId, user._id.toString(), user.email);
return this.response(data);
}
//uploader
@ApiOperation("Upload a order post receipt info image ==> need to login as seller")
@ApiResponse("File uploaded successfully", HttpStatus.Accepted)
@ApiFile("image")
@ApiAuth()
@httpPost("/image/upload", Guard.authSeller(), UploadService.single("image", "shipping-receipt"))
public async uploadImage(@request() req: Request) {
// eslint-disable-next-line no-undef
const file = req.file as Express.MulterS3.File;
const data = {
message: "file uploaded!",
url: {
size: file?.size,
url: file?.location,
type: file?.mimetype,
},
};
return this.response({ data }, HttpStatus.Accepted);
}
}
export { OrderController };
File diff suppressed because it is too large Load Diff
+444
View File
@@ -0,0 +1,444 @@
import { inject, injectable } from "inversify";
import { ClientSession, startSession } from "mongoose";
import { InsertTrackCodeDTO, InsertTrackCodeGroupDTO } from "./DTO/insertTrackCode.dto";
import { ReceivedOrderItemDTO } from "./DTO/received.dto";
import { SellerOrdersDTO } from "./DTO/sellerOrder.dto";
import { UserOrderDTO } from "./DTO/userOrder.dto";
import { IShipmentAddress } from "./models/Abstraction/IOrder";
import { IOrderItem } from "./models/Abstraction/IOrderItem";
import { OrderItemRepo, OrderRepository } from "./order.repository";
import { AddressMessage, OrderMessage, ShopMessage, UserMessage } from "../../common/enums/message.enum";
import { OrderItemsStatus, OrdersStatus } from "../../common/enums/order.enum";
import { OrderStatusQuery, SellerOrdersQueries } from "../../common/types/query.type";
import { BadRequestError } from "../../core/app/app.errors";
import { IOCTYPES } from "../../IOC/ioc.types";
import { OrderQueue } from "../../queues/order/OrderQueue";
import { EmailService } from "../../utils/email.service";
import { AddressService } from "../address/address.service";
import { ICity } from "../address/models/city.model";
import { IProvince } from "../address/models/province.model";
import { ICartShipmentItem } from "../cart/models/Abstraction/ICartShipmentItem";
import { ICartPayment } from "../payment/models/Abstraction/IPayments";
import { OwnerRef } from "../shop/models/Abstraction/IShop";
import { ShopRepo } from "../shop/shop.repository";
import { IUser } from "../user/models/Abstraction/IUser";
import { WalletService } from "../wallet/wallet.service";
@injectable()
class OrderService {
@inject(IOCTYPES.OrderRepository) private orderRepo: OrderRepository;
@inject(IOCTYPES.OrderItemRepo) private orderItemRepo: OrderItemRepo;
@inject(IOCTYPES.ShopRepo) private shopRepo: ShopRepo;
@inject(IOCTYPES.AddressService) private addressService: AddressService;
@inject(IOCTYPES.WalletService) private walletService: WalletService;
//#######################################################
//#######################################################
async updateOrderStatus(orderId: number, status: OrdersStatus, session: ClientSession) {
return await this.orderRepo.model.findByIdAndUpdate(orderId, { orderStatus: status }, { session });
}
//#######################################################
//#######################################################
async getOrderItemByOrderId(orderId: number, session: ClientSession) {
return await this.orderItemRepo.model.find({ order: orderId }, null, { session });
}
//#######################################################
//#######################################################
async getOrderByPaymentId(paymentId: string, session: ClientSession) {
return await this.orderRepo.model.findOne({ payment: paymentId }).session(session);
}
//#######################################################
//#######################################################
async createOrder(user: IUser, payment: ICartPayment, cartShipmentItems: ICartShipmentItem[], session: ClientSession) {
if (!user.address) throw new BadRequestError([AddressMessage.UserShouldHaveAddress, AddressMessage.NotFound]);
const userAddress = await this.addressService.getUserAddress(user.address.toString());
const city = userAddress.city as unknown as ICity;
const province = userAddress.province as unknown as IProvince;
const shipmentAddress: IShipmentAddress = {
address: userAddress.address,
city: city.name,
province: province.name,
phone: user.phoneNumber,
plaque: userAddress.plaque,
postalCode: userAddress.postalCode,
};
const order = await this.orderRepo.model.create(
[
{
user: user._id.toString(),
payment: payment._id,
shipmentAddress,
},
],
{ session },
);
const orderItemsData: IOrderItem[] = cartShipmentItems.map((item) => ({
order: order[0]._id,
shop: item.shop,
shipmentItems: item.shipmentItems,
shipper: item.shipper,
totalSellingPrice: item.totalSellingPrice,
totalPaymentPrice: item.totalPaymentPrice,
totalRetailPrice: item.totalRetailPrice,
totalCouponDiscount: item.totalCouponDiscount,
totalShipmentCost: item.totalShipmentCost,
itemsCount: item.itemsCount,
status: OrderItemsStatus.Processing,
}));
const orderItems = await this.orderItemRepo.model.create(orderItemsData, { session });
// Add the order to the queue with a 5-minute delay
await OrderQueue.addOrderToQueue(order[0]._id, 5 * 60 * 1000);
return {
order: order[0],
orderItems,
};
}
//#######################################################
//#######################################################
async getAdminOrders(sellerId: string, queries: SellerOrdersQueries) {
const shop = await this.shopRepo.model.findOne({ owner: sellerId });
if (!shop) throw new BadRequestError(ShopMessage.ShopNotFound);
const priceRange = await this.orderItemRepo.getOrderPriceRange(shop._id.toString());
const { docs, count } = await this.orderItemRepo.getOrdersForSellerPanel(shop._id.toString(), queries);
const orders = docs.map((doc: any) => SellerOrdersDTO.transformOrder(doc));
return { orders, count, priceRange };
}
//#######################################################
//#######################################################
async getSellerOrders(ownerRef: OwnerRef, queries: SellerOrdersQueries) {
const shops = await this.shopRepo.model.find({ ownerRef }).select("_id");
const shopIds = shops.map((shop) => shop._id);
const priceRange = await this.orderItemRepo.getSellerOrderPriceRange(shopIds);
const { docs, count } = await this.orderItemRepo.getSellerOrdersForAdminPanel(shopIds, queries);
const orders = docs.map((doc: any) => SellerOrdersDTO.transformOrder(doc));
return { orders, count, priceRange };
}
//#######################################################
//#######################################################
async getOrders(sellerId: string, queries: SellerOrdersQueries, orderIds?: string[] | string) {
const shop = await this.shopRepo.model.findOne({ owner: sellerId });
if (!shop) throw new BadRequestError(ShopMessage.ShopNotFound);
const priceRange = await this.orderItemRepo.getOrderPriceRange(shop._id.toString());
const { docs, count } = await this.orderItemRepo.getOrdersForSellerPanel(shop._id.toString(), queries, orderIds);
const orders = docs.map((doc: any) => SellerOrdersDTO.transformOrder(doc));
return { orders, count, priceRange };
}
//#######################################################
//#######################################################
async insertTrackCode(orderId: number, sellerId: string, insertDto: InsertTrackCodeDTO) {
const { orderItem } = await this.validateSellerOrder(orderId, sellerId);
orderItem.trackCode = insertDto.trackCode;
orderItem.postingDate = insertDto.postingDate;
orderItem.postingReceipt = insertDto.postingReceipt;
orderItem.status = OrderItemsStatus.Shipped;
await orderItem.save();
return {
message: OrderMessage.TrackingDetailInserted,
orderItem,
};
}
//#######################################################
async insertTrackCodeGroup(sellerId: string, insertDto: InsertTrackCodeGroupDTO) {
const session = await startSession();
session.startTransaction();
try {
const orderIds = insertDto.trackCodes.map((tc) => tc.orderId);
const shop = await this.shopRepo.model.findOne({ owner: sellerId, ownerRef: OwnerRef.SELLER });
if (!shop) throw new BadRequestError(ShopMessage.ShopNotFound);
const orderItems = await this.orderItemRepo.model.find({ order: { $in: orderIds }, shop: shop._id }).session(session);
if (orderItems.length !== orderIds.length) throw new BadRequestError(OrderMessage.OrderItemsNotFound);
// Prepare bulk operations
const bulkOps = insertDto.trackCodes.map((trackCodeDto) => ({
updateOne: {
filter: {
order: trackCodeDto.orderId,
shop: shop._id,
},
update: {
$set: {
trackCode: trackCodeDto.trackCode,
postingDate: trackCodeDto.postingDate,
postingReceipt: trackCodeDto.postingReceipt,
status: OrderItemsStatus.Shipped,
},
},
},
}));
// Execute bulk operation
await this.orderItemRepo.model.bulkWrite(bulkOps, { session });
await session.commitTransaction();
return {
message: OrderMessage.TrackingDetailInserted,
};
} catch (error) {
await session.abortTransaction();
throw error;
} finally {
await session.endSession();
}
}
//#######################################################
//#######################################################
async insertNativeShopTrackCode(orderId: number, adminId: string, insertDto: InsertTrackCodeDTO) {
const { orderItem } = await this.validateSellerOrder(orderId, adminId);
orderItem.trackCode = insertDto.trackCode;
orderItem.postingDate = insertDto.postingDate;
orderItem.postingReceipt = insertDto.postingReceipt;
orderItem.status = OrderItemsStatus.Shipped;
await orderItem.save();
return {
message: OrderMessage.TrackingDetailInserted,
orderItem,
};
}
//#######################################################
//#######################################################
async trackOrder(orderId: number, userId: string, userEmail: string) {
if (isNaN(orderId)) throw new BadRequestError(OrderMessage.NotFound);
if (!userEmail) throw new BadRequestError(UserMessage.EmailNotExist);
const order = await this.orderRepo.model.findOne({ _id: orderId, user: userId });
if (!order) throw new BadRequestError(OrderMessage.NotFound);
const trackingDetails = await this.orderItemRepo.getOrderTrackingDetails(orderId);
if (!trackingDetails.length) throw new BadRequestError(OrderMessage.TrackingCodeNotFound);
const trackingInfo = trackingDetails.map((item: any) => ({
trackCode: item.trackCode,
status: item.status,
shipper: item.shipper.name,
postingDate: item.postingDate,
}));
await EmailService.sendOrderTrackingEmail(userEmail, { orderId, trackingInfo });
return {
message: OrderMessage.TrackingEmailSent,
};
}
//#######################################################
async setOrderItemReceived(orderId: number, userId: string, receivedOrderItemDto: ReceivedOrderItemDTO) {
const session = await startSession();
session.startTransaction();
try {
await this.validateUserOrder(orderId, userId);
const orderItem = await this.orderItemRepo.model.findById(receivedOrderItemDto.orderItemId).session(session);
if (!orderItem) throw new BadRequestError(`OrderItem with ID ${receivedOrderItemDto.orderItemId} not found.`);
if (orderItem.status === OrderItemsStatus.Delivered) throw new BadRequestError(OrderMessage.ItemAlreadyReceived);
if (orderItem.status !== OrderItemsStatus.Shipped) throw new BadRequestError(OrderMessage.ItemNotShipped);
orderItem.status = OrderItemsStatus.Delivered;
const shop = await this.shopRepo.model.findById(orderItem.shop.toString()).session(session);
if (!shop) throw new BadRequestError(ShopMessage.ShopNotFoundById);
if (shop.ownerRef === OwnerRef.SELLER) {
await this.walletService.creditSellerWalletForOrder(shop.owner.toString(), orderId, orderItem.totalPaymentPrice, session);
}
await orderItem.save({ session });
await session.commitTransaction();
await session.endSession();
return {
message: OrderMessage.ItemReceived,
};
} catch (error) {
await session.abortTransaction();
await session.endSession();
throw error;
}
}
//#######################################################
//#######################################################
async getSellerOrderDetail(orderId: number, sellerId: string) {
const { shop } = await this.validateSellerOrder(orderId, sellerId);
const doc = await this.orderItemRepo.getSellerOrderDetail(orderId, shop._id.toString());
const order = SellerOrdersDTO.transformOrder(doc[0]);
return { order };
}
//#######################################################
//#######################################################
async getUserOrderDetail(orderId: number, userId: string) {
await this.validateUserOrder(orderId, userId);
const doc = await this.orderRepo.getUserOrderDetail(orderId, userId);
const order = UserOrderDTO.transformOrder(doc[0]);
return { order };
}
async getUserOrderItemsDetail(orderId: number, orderItemId: string, userId: string) {
await this.validateUserOrder(orderId, userId);
const doc = await this.orderItemRepo.getUserOrderItemsDetail(orderId, orderItemId);
const orderItems = UserOrderDTO.transformOrder(doc[0]);
return { orderItems };
}
//#######################################################
//#######################################################
async getSellerSalesStats(sellerId: string, queries: { start: string; end: string }) {
const shop = await this.shopRepo.model.findOne({ owner: sellerId });
if (!shop) throw new BadRequestError(ShopMessage.ShopNotFound);
const saleStats = await this.orderItemRepo.getSellerSalesStats(shop._id.toString(), queries);
return { saleStats };
}
//#######################################################
//#######################################################
async getUserOrders(userId: string, statusQuery: OrderStatusQuery) {
// const orders = await this.orderRepo.model.find({ user: userId });
const docs = await this.orderRepo.getUserOrders(userId, statusQuery);
const orders = docs.map((doc) => UserOrderDTO.transformOrder(doc));
const mappedOrders = orders.map((order) => this.mapUserOrderItems(order));
return { mappedOrders, orders };
}
//helper methods
//#######################################################
//#######################################################
private async validateSellerOrder(orderId: number, sellerId: string) {
const order = await this.orderRepo.findById(orderId);
if (!order) throw new BadRequestError(OrderMessage.NotFound);
const shop = await this.shopRepo.model.findOne({ owner: sellerId });
if (!shop) throw new BadRequestError(ShopMessage.ShopNotFound);
const orderItem = await this.orderItemRepo.model.findOne({ shop: shop._id.toString(), order: order._id });
if (!orderItem) throw new BadRequestError(OrderMessage.OrderItemsNotFound);
return { order, orderItem, shop };
}
//#######################################################
//#######################################################
private async validateUserOrder(orderId: number, userId: string) {
const order = await this.orderRepo.model.findOne({ _id: orderId, user: userId });
if (!order) throw new BadRequestError(OrderMessage.NotFound);
const orderItems = await this.orderItemRepo.model.find({ order: order._id });
if (!orderItems || orderItems.length === 0) throw new BadRequestError(OrderMessage.OrderItemsNotFound);
return { order, orderItems };
}
//#######################################
//#######################################
private mapUserOrderItems(userOrder: UserOrderDTO) {
const shipmentItems = userOrder.orderItems.map((orderItem) => {
return orderItem.shipmentItems;
});
return {
orderId: userOrder._id,
user: userOrder.user,
orderItems: shipmentItems.flat(Infinity),
payment: userOrder.payment,
shipmentAddress: userOrder.shipmentAddress,
orderStatus: userOrder.orderStatus,
createdAt: userOrder.createdAt,
};
}
//#######################################
//#######################################
async getDailySalesReport() {
const sales = await this.orderItemRepo.getDailySalesReport();
return sales;
}
async getWeeklySalesReport() {
const sales = await this.orderItemRepo.getWeeklySalesReport();
return sales;
}
async getMonthlySalesReport() {
const sales = await this.orderItemRepo.getMonthlySalesReport();
return sales;
}
async getAnnualSalesReport() {
const sales = await this.orderItemRepo.getAnnualSalesReport();
return sales;
}
async getDailyShipmentSalesReport() {
const sales = await this.orderItemRepo.getDailyShipmentSalesReport();
return sales;
}
async getWeeklyShipmentSalesReport() {
const sales = await this.orderItemRepo.getWeeklyShipmentSalesReport();
return sales;
}
async getMonthlyShipmentSalesReport() {
const sales = await this.orderItemRepo.getMonthlyShipmentSalesReport();
return sales;
}
async getAnnualShipmentSalesReport() {
const sales = await this.orderItemRepo.getAnnualShipmentSalesReport();
return sales;
}
async getFiveDaysAgoSalesReport() {
const sales = await this.orderItemRepo.getFiveDaysAgoSalesReport();
return sales;
}
async getTopSellingProductsS() {
const sales = await this.orderItemRepo.getTopSellingProducts();
return sales;
}
async getAllProcessingOrders() {
const sales = await this.orderItemRepo.getAllProcessingOrders();
return sales;
}
}
export { OrderService };