first
This commit is contained in:
@@ -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[];
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 };
|
||||
@@ -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
@@ -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 };
|
||||
Reference in New Issue
Block a user