492 lines
19 KiB
TypeScript
492 lines
19 KiB
TypeScript
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 getUserOrderDetailAsAdmin(orderId: number) {
|
|
const { order } = await this.validateAdminUserOrder(orderId);
|
|
|
|
const doc = await this.orderRepo.getUserOrderDetail(orderId, order.user._id.toString());
|
|
const cleanedOrder = SellerOrdersDTO.transformOrder(doc[0]);
|
|
|
|
return { order: cleanedOrder };
|
|
}
|
|
//#######################################################
|
|
//#######################################################
|
|
|
|
async getSellerOrderDetailAsAdmin(orderId: number) {
|
|
const { shopIds } = await this.validateAdminSellerOrder(orderId);
|
|
|
|
const doc = await this.orderItemRepo.getSellerOrderDetailForAdminPanel(orderId, shopIds);
|
|
|
|
const cleanedOrder = SellerOrdersDTO.transformOrder(doc[0]);
|
|
|
|
return { order: cleanedOrder };
|
|
}
|
|
//#######################################################
|
|
//#######################################################
|
|
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 docs = await this.orderRepo.getUserOrders(userId, statusQuery);
|
|
const orders = docs.map((doc) => UserOrderDTO.transformOrder(doc));
|
|
const mappedOrders = orders.map((order) => this.mapUserOrderItems(order));
|
|
const userOrderCount = await this.orderRepo.getUserOrderCount(userId);
|
|
return { mappedOrders, orders, userOrderCount };
|
|
}
|
|
|
|
//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 async validateAdminUserOrder(orderId: number) {
|
|
const order = await this.orderRepo.model.findOne({ _id: orderId });
|
|
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 async validateAdminSellerOrder(orderId: number) {
|
|
const order = await this.orderRepo.model.findOne({ _id: orderId }).lean();
|
|
if (!order) throw new BadRequestError(OrderMessage.NotFound);
|
|
|
|
const orderItems = await this.orderItemRepo.model.find({ order: orderId }).lean();
|
|
const shopIds = orderItems.map((item) => item.shop._id);
|
|
|
|
const orderItem = await this.orderItemRepo.model.findOne({ shop: { in: shopIds }, order: order._id });
|
|
if (!orderItem) throw new BadRequestError(OrderMessage.OrderItemsNotFound);
|
|
return { order, orderItem, shopIds };
|
|
}
|
|
|
|
//#######################################
|
|
//#######################################
|
|
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 };
|