feat(F-048): completed feature

This commit is contained in:
chattie
2026-08-19 07:17:14 +02:00
parent 8ee1938af9
commit 835ab66eda
187 changed files with 12361 additions and 1065 deletions

View File

@@ -1,10 +1,12 @@
import type { FastifyInstance } from 'fastify';
import type { FastifySchema } from 'fastify';
import type pg from 'pg';
import { z } from 'zod';
import type { Authenticate, CurrentUser } from '../../../shared/auth.js';
import type { Authenticate } from '../../../shared/auth.js';
import { requireRole } from '../../../shared/auth.js';
import { AppError } from '../../../shared/errors.js';
import { parseJson } from '../../../shared/http-input.js';
import { errorSchema } from '../../../shared/swagger.js';
import { OrderService } from '../application/order-service.js';
import { OrderNotFoundError, OrderStateTransitionError } from '../domain/errors.js';
import type { OrderState } from '../domain/order.js';
@@ -63,7 +65,14 @@ export async function registerOrdersRoutes(
): Promise<void> {
const service = new OrderService(new PgOrderRepository(deps.pool), new NoOpOrderEventPublisher());
app.post('/orders', async (request, reply) => {
const createOrderSwaggerSchema: FastifySchema = {
tags: ['Orders'],
summary: 'Create order',
description: 'Crea un pedido. Requiere autenticación.',
body: { type: 'object' },
response: { 201: { type: 'object' }, 401: errorSchema },
};
app.post('/orders', { schema: createOrderSwaggerSchema }, async (request, reply) => {
const user = await deps.authenticate(request);
const input = parseJson(createOrderSchema, request.body);
try {
@@ -79,7 +88,37 @@ export async function registerOrdersRoutes(
}
});
app.post('/orders/:id/transitions', async (request, reply) => {
const transitionOrderSchema: FastifySchema = {
tags: ['Orders'],
summary: 'Transition order state (customer)',
params: {
type: 'object',
required: ['id'],
properties: { id: { type: 'string', format: 'uuid' } },
},
body: {
type: 'object',
required: ['state'],
properties: {
state: {
type: 'string',
enum: [
'PENDING',
'AWAITING_PAYMENT',
'PAID',
'PROCESSING',
'SHIPPED',
'DELIVERED',
'CANCELLED',
'REFUNDED',
'PARTIALLY_REFUNDED',
],
},
},
},
response: { 401: errorSchema, 404: errorSchema },
};
app.post('/orders/:id/transitions', { schema: transitionOrderSchema }, async (request, reply) => {
const user = await deps.authenticate(request);
const { id } = parseJson(orderIdParamSchema, request.params);
const { state } = parseJson(transitionSchema, request.body);
@@ -91,7 +130,18 @@ export async function registerOrdersRoutes(
}
});
app.get('/orders/:id', async (request, reply) => {
const getOrderSchema: FastifySchema = {
tags: ['Orders'],
summary: 'Get order',
description: 'Obtiene un pedido. El cliente solo ve los suyos; admin ve cualquiera.',
params: {
type: 'object',
required: ['id'],
properties: { id: { type: 'string', format: 'uuid' } },
},
response: { 401: errorSchema, 404: errorSchema },
};
app.get('/orders/:id', { schema: getOrderSchema }, async (request, reply) => {
const user = await deps.authenticate(request);
const { id } = parseJson(orderIdParamSchema, request.params);
const order = await service.getOrder(id, user.id);
@@ -100,14 +150,29 @@ export async function registerOrdersRoutes(
});
// Admin-only routes
app.get('/orders', async (request, reply) => {
const listOrdersSchema: FastifySchema = {
tags: ['Orders'],
summary: 'List orders (admin)',
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));
});
app.get('/orders/:id/admin', async (request, reply) => {
const getOrderAdminSchema: FastifySchema = {
tags: ['Orders'],
summary: 'Get order (admin)',
params: {
type: 'object',
required: ['id'],
properties: { id: { type: 'string', format: 'uuid' } },
},
response: { 401: errorSchema, 403: errorSchema, 404: errorSchema },
};
app.get('/orders/:id/admin', { schema: getOrderAdminSchema }, async (request, reply) => {
const user = await deps.authenticate(request);
requireRole(user, 'admin');
const { id } = parseJson(orderIdParamSchema, request.params);
@@ -116,18 +181,53 @@ export async function registerOrdersRoutes(
return reply.send(serializeOrder(order));
});
app.post('/orders/:id/transitions/admin', async (request, reply) => {
const user = await deps.authenticate(request);
requireRole(user, 'admin');
const { id } = parseJson(orderIdParamSchema, request.params);
const { state } = parseJson(transitionSchema, request.body);
try {
const order = await service.transitionAdmin(id, state as OrderState);
return reply.send(serializeOrder(order));
} catch (error) {
throw mapOrderError(error);
}
});
const adminTransitionSchema: FastifySchema = {
tags: ['Orders'],
summary: 'Force order transition (admin)',
description: 'Fuerza una transición de estado de pedido.',
params: {
type: 'object',
required: ['id'],
properties: { id: { type: 'string', format: 'uuid' } },
},
body: {
type: 'object',
required: ['state'],
properties: {
state: {
type: 'string',
enum: [
'PENDING',
'AWAITING_PAYMENT',
'PAID',
'PROCESSING',
'SHIPPED',
'DELIVERED',
'CANCELLED',
'REFUNDED',
'PARTIALLY_REFUNDED',
],
},
},
},
response: { 401: errorSchema, 403: errorSchema, 404: errorSchema },
};
app.post(
'/orders/:id/transitions/admin',
{ schema: adminTransitionSchema },
async (request, reply) => {
const user = await deps.authenticate(request);
requireRole(user, 'admin');
const { id } = parseJson(orderIdParamSchema, request.params);
const { state } = parseJson(transitionSchema, request.body);
try {
const order = await service.transitionAdmin(id, state as OrderState);
return reply.send(serializeOrder(order));
} catch (error) {
throw mapOrderError(error);
}
},
);
}
function mapOrderError(error: unknown): Error {

View File

@@ -54,7 +54,8 @@ export class OrderService implements OrderServicePort {
throw new OrderStateTransitionError(existing.state, next);
const updated = await this.repo.updateState(id, next);
if (!updated) throw new OrderNotFoundError();
if (next === 'PAID') await this.events.emit({ type: 'OrderPaid', orderId: id, userId: existing.userId });
if (next === 'PAID')
await this.events.emit({ type: 'OrderPaid', orderId: id, userId: existing.userId });
if (next === 'CANCELLED')
await this.events.emit({ type: 'OrderCancelled', orderId: id, userId: existing.userId });
return updated;