feat(F-103): completed feature
This commit is contained in:
@@ -153,13 +153,30 @@ export async function registerOrdersRoutes(
|
||||
const listOrdersSchema: FastifySchema = {
|
||||
tags: ['Orders'],
|
||||
summary: 'List orders (admin)',
|
||||
querystring: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
status: { type: 'string', description: 'Filtro por estado del pedido' },
|
||||
q: { type: 'string', maxLength: 120, description: 'Búsqueda por ID o email del cliente' },
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 20 },
|
||||
offset: { type: 'integer', minimum: 0, default: 0 },
|
||||
},
|
||||
},
|
||||
response: { 401: errorSchema, 403: errorSchema },
|
||||
};
|
||||
app.get('/orders', { schema: listOrdersSchema }, async (request, reply) => {
|
||||
const user = await deps.authenticate(request);
|
||||
requireRole(user, 'admin');
|
||||
const orders = await service.listOrders();
|
||||
return reply.send(orders.map(serializeOrder));
|
||||
const query = request.query as { status?: string; q?: string; limit?: string; offset?: string };
|
||||
const limit = Math.min(Math.max(Number(query.limit ?? 20) || 20, 1), 100);
|
||||
const offset = Math.max(Number(query.offset ?? 0) || 0, 0);
|
||||
const { items, total } = await service.searchOrders({
|
||||
state: query.status?.trim() || undefined,
|
||||
q: query.q?.trim() || undefined,
|
||||
limit,
|
||||
offset,
|
||||
});
|
||||
return reply.send({ items: items.map(serializeOrder), total });
|
||||
});
|
||||
|
||||
const getOrderAdminSchema: FastifySchema = {
|
||||
|
||||
@@ -26,6 +26,19 @@ export class OrderService implements OrderServicePort {
|
||||
return this.repo.findAll();
|
||||
}
|
||||
|
||||
async searchOrders(filters: {
|
||||
state?: string;
|
||||
q?: string;
|
||||
limit: number;
|
||||
offset: number;
|
||||
}): Promise<{ items: OrderView[]; total: number }> {
|
||||
if (!this.repo.search) {
|
||||
const all = await this.repo.findAll();
|
||||
return { items: all.slice(filters.offset, filters.offset + filters.limit), total: all.length };
|
||||
}
|
||||
return this.repo.search(filters);
|
||||
}
|
||||
|
||||
async transition(id: string, next: OrderState, userId: string): Promise<OrderView> {
|
||||
const existing = await this.repo.findByIdAndUserId(id, userId);
|
||||
if (!existing) throw new OrderNotFoundError();
|
||||
|
||||
@@ -8,6 +8,12 @@ export interface OrderRepository {
|
||||
totals: { subtotalCents: number; discountCents: number; taxCents: number; totalCents: number };
|
||||
}): Promise<OrderView>;
|
||||
findAll(): Promise<OrderView[]>;
|
||||
search?(filters: {
|
||||
state?: string;
|
||||
q?: string;
|
||||
limit: number;
|
||||
offset: number;
|
||||
}): Promise<{ items: OrderView[]; total: number }>;
|
||||
findById(id: string): Promise<OrderView | undefined>;
|
||||
findByIdAndUserId(id: string, userId: string): Promise<OrderView | undefined>;
|
||||
updateState(id: string, state: OrderState): Promise<OrderView | undefined>;
|
||||
@@ -25,6 +31,12 @@ export type OrderEvent =
|
||||
export interface OrderServicePort {
|
||||
create(input: CreateOrderCommand): Promise<OrderView>;
|
||||
listOrders(): Promise<OrderView[]>;
|
||||
searchOrders?(filters: {
|
||||
state?: string;
|
||||
q?: string;
|
||||
limit: number;
|
||||
offset: number;
|
||||
}): Promise<{ items: OrderView[]; total: number }>;
|
||||
transition(id: string, next: OrderState, userId: string): Promise<OrderView>;
|
||||
transitionAdmin(id: string, next: OrderState): Promise<OrderView>;
|
||||
getOrder(id: string, userId: string): Promise<OrderView | undefined>;
|
||||
|
||||
@@ -103,6 +103,52 @@ export class PgOrderRepository implements OrderRepository {
|
||||
);
|
||||
}
|
||||
|
||||
async search(filters: {
|
||||
state?: string;
|
||||
q?: string;
|
||||
limit: number;
|
||||
offset: number;
|
||||
}): Promise<{ items: OrderView[]; total: number }> {
|
||||
const where: string[] = [];
|
||||
const values: unknown[] = [];
|
||||
if (filters.state) {
|
||||
values.push(filters.state);
|
||||
where.push(`o.state = $${values.length}`);
|
||||
}
|
||||
if (filters.q?.trim()) {
|
||||
values.push(`%${filters.q.trim()}%`);
|
||||
where.push(`(o.id::text ILIKE $${values.length} OR u.email ILIKE $${values.length})`);
|
||||
}
|
||||
const whereSql = where.length ? `WHERE ${where.join(' AND ')}` : '';
|
||||
const countResult = await this.pool.query<{ count: string }>(
|
||||
`SELECT COUNT(*)::text AS count
|
||||
FROM orders_orders o
|
||||
LEFT JOIN identity_users u ON u.id = o.user_id
|
||||
${whereSql}`,
|
||||
values,
|
||||
);
|
||||
const total = Number(countResult.rows[0]?.count ?? '0');
|
||||
values.push(filters.limit, filters.offset);
|
||||
const result = await this.pool.query<OrderRow>(
|
||||
`SELECT o.* FROM orders_orders o
|
||||
LEFT JOIN identity_users u ON u.id = o.user_id
|
||||
${whereSql}
|
||||
ORDER BY o.created_at DESC
|
||||
LIMIT $${values.length - 1} OFFSET $${values.length}`,
|
||||
values,
|
||||
);
|
||||
const items = await Promise.all(
|
||||
result.rows.map(async (order) => {
|
||||
const itemResult = await this.pool.query<ItemRow>(
|
||||
'SELECT * FROM orders_items WHERE order_id = $1 ORDER BY id',
|
||||
[order.id],
|
||||
);
|
||||
return { ...toOrder(order), items: itemResult.rows.map(toItem) };
|
||||
}),
|
||||
);
|
||||
return { items, total };
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<OrderView | undefined> {
|
||||
const orderResult = await this.pool.query<OrderRow>(
|
||||
'SELECT * FROM orders_orders WHERE id = $1',
|
||||
|
||||
Reference in New Issue
Block a user