feat(F-103): completed feature

This commit is contained in:
chattie
2026-08-21 07:55:18 +02:00
parent 5177a851aa
commit c07776822d
40 changed files with 886 additions and 156 deletions

View File

@@ -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 = {

View File

@@ -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();

View File

@@ -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>;

View File

@@ -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',