185 lines
5.6 KiB
TypeScript
185 lines
5.6 KiB
TypeScript
import { Injectable } from '@nestjs/common';
|
|
import { EntityManager, EntityRepository } from '@mikro-orm/postgresql';
|
|
import { FilterQuery } from '@mikro-orm/core';
|
|
import { Order } from '../entities/order.entity';
|
|
import { PaginatedResult } from 'src/common/interfaces/pagination.interface';
|
|
import { OrderStatus } from '../interface/order.interface';
|
|
import { PaymentMethodEnum, PaymentStatusEnum } from '../../payment/interface/payment';
|
|
import { Review } from '../../review/entities/review.entity';
|
|
|
|
type FindOrdersOpts = {
|
|
page?: number;
|
|
limit?: number;
|
|
statuses?: OrderStatus[];
|
|
paymentStatus?: PaymentStatusEnum;
|
|
search?: string;
|
|
startDate?: string;
|
|
endDate?: string;
|
|
orderBy?: string;
|
|
order?: 'asc' | 'desc';
|
|
userId?: string;
|
|
excludeOnlinePendingPayment?: boolean;
|
|
};
|
|
|
|
@Injectable()
|
|
export class OrderRepository extends EntityRepository<Order> {
|
|
constructor(readonly em: EntityManager) {
|
|
super(em, Order);
|
|
}
|
|
|
|
/**
|
|
* Find orders with pagination and optional filters.
|
|
* Supports: statuses, paymentStatus, search (orderNumber), date range, ordering.
|
|
*/
|
|
async findAllPaginated(restId: string, opts: FindOrdersOpts = {}): Promise<PaginatedResult<Order>> {
|
|
const {
|
|
page = 1,
|
|
limit = 10,
|
|
statuses,
|
|
search,
|
|
startDate,
|
|
endDate,
|
|
orderBy = 'createdAt',
|
|
order = 'desc',
|
|
userId,
|
|
excludeOnlinePendingPayment = false,
|
|
} = opts;
|
|
|
|
const offset = (page - 1) * limit;
|
|
|
|
const where: FilterQuery<Order> = { restaurant: { id: restId } };
|
|
|
|
// Filter by statuses
|
|
if (statuses) {
|
|
// Ensure statuses is always an array and normalize it
|
|
const normalizedStatuses = Array.isArray(statuses) ? statuses : [statuses];
|
|
// Filter out any empty values
|
|
const validStatuses = normalizedStatuses.filter((s): s is OrderStatus => !!s);
|
|
|
|
if (validStatuses.length > 0) {
|
|
// Use direct equality for single value, $in for multiple values to avoid SQL generation issues
|
|
if (validStatuses.length === 1) {
|
|
where.status = validStatuses[0];
|
|
} else {
|
|
where.status = { $in: validStatuses };
|
|
}
|
|
}
|
|
}
|
|
|
|
if (userId) {
|
|
where.user = { id: userId };
|
|
}
|
|
|
|
// Filter by date range
|
|
if (startDate || endDate) {
|
|
where.createdAt = {};
|
|
if (startDate) {
|
|
where.createdAt.$gte = new Date(startDate);
|
|
}
|
|
if (endDate) {
|
|
where.createdAt.$lte = new Date(endDate);
|
|
}
|
|
}
|
|
|
|
// Search by order number or user information
|
|
if (search) {
|
|
const searchPattern = `%${search}%`;
|
|
const searchConditions: any[] = [
|
|
{ user: { firstName: { $ilike: searchPattern } } },
|
|
{ user: { lastName: { $ilike: searchPattern } } },
|
|
{ user: { phone: { $ilike: searchPattern } } },
|
|
];
|
|
|
|
// If search is numeric, also search by orderNumber
|
|
const numericSearch = Number(search);
|
|
if (!isNaN(numericSearch) && isFinite(numericSearch)) {
|
|
searchConditions.push({ orderNumber: numericSearch });
|
|
}
|
|
|
|
where.$or = searchConditions;
|
|
}
|
|
|
|
// Filter: Exclude orders with payment method Online and status pending_payment
|
|
if (excludeOnlinePendingPayment) {
|
|
const existingConditions = where.$and || [];
|
|
where.$and = [
|
|
...existingConditions,
|
|
{
|
|
$or: [
|
|
{ paymentMethod: { method: { $ne: PaymentMethodEnum.Online } } },
|
|
{ status: { $ne: OrderStatus.PENDING_PAYMENT } },
|
|
],
|
|
},
|
|
];
|
|
}
|
|
|
|
// First, fetch orders without reviews
|
|
const [data, total] = await this.findAndCount(where, {
|
|
limit,
|
|
offset,
|
|
orderBy: { [orderBy]: order.toLowerCase() as 'asc' | 'desc' },
|
|
populate: ['user', 'restaurant', 'deliveryMethod', 'paymentMethod', 'items', 'items.product'] as never,
|
|
});
|
|
|
|
// Collect all (orderId, productId) pairs for efficient review lookup
|
|
const orderproductPairs: Array<{ orderId: string; productId: string }> = [];
|
|
for (const order of data) {
|
|
for (const item of order.items.getItems()) {
|
|
if (item.product?.id) {
|
|
orderproductPairs.push({ orderId: order.id, productId: item.product.id });
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fetch all relevant reviews in a single query
|
|
const reviewsMap: Map<string, string> = new Map();
|
|
if (orderproductPairs.length > 0) {
|
|
const orderIds = [...new Set(orderproductPairs.map(p => p.orderId))];
|
|
const productIds = [...new Set(orderproductPairs.map(p => p.productId))];
|
|
|
|
const reviews = await this.em.find(
|
|
Review,
|
|
{
|
|
order: { id: { $in: orderIds } },
|
|
product: { id: { $in: productIds } },
|
|
},
|
|
{
|
|
fields: ['id', 'order', 'product'],
|
|
populate: ['order', 'product'],
|
|
},
|
|
);
|
|
|
|
// Create a map: key = `${orderId}-${productId}`, value = reviewId
|
|
for (const review of reviews) {
|
|
const key = `${review.order.id}-${review.product.id}`;
|
|
reviewsMap.set(key, review.id);
|
|
}
|
|
}
|
|
|
|
// Map reviewIds to products
|
|
for (const order of data) {
|
|
for (const item of order.items.getItems()) {
|
|
if (item.product) {
|
|
const key = `${order.id}-${item.product.id}`;
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
const product = item.product as any;
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
product.reviewId = reviewsMap.get(key) || null;
|
|
}
|
|
}
|
|
}
|
|
|
|
const totalPages = Math.ceil(total / limit);
|
|
|
|
return {
|
|
data,
|
|
meta: {
|
|
total,
|
|
page,
|
|
limit,
|
|
totalPages,
|
|
},
|
|
};
|
|
}
|
|
}
|