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,8 +1,10 @@
import type { FastifyInstance } from 'fastify';
import type { FastifySchema } from 'fastify';
import type pg from 'pg';
import { z } from 'zod';
import { requireRole, type Authenticate } from '../../../shared/auth.js';
import { AppError } from '../../../shared/errors.js';
import { errorSchema } from '../../../shared/swagger.js';
import { parseJson } from '../../../shared/http-input.js';
import { InventoryService } from '../application/inventory-service.js';
import {
@@ -39,14 +41,43 @@ export async function registerInventoryRoutes(
): Promise<void> {
const inventory = new InventoryService(new PgInventoryRepository(deps.pool));
app.get('/inventory/:variantId/availability', async (request, reply) => {
const { variantId } = parseJson(variantParamSchema, request.params);
const { quantity } = parseJson(availabilityQuerySchema, request.query);
const availability = await inventory.checkAvailability(variantId, quantity);
return reply.send(availability);
});
const availabilitySchema: FastifySchema = {
tags: ['Inventory'],
summary: 'Check availability (público)',
params: {
type: 'object',
required: ['variantId'],
properties: { variantId: { type: 'string', format: 'uuid' } },
},
querystring: { type: 'object', properties: { quantity: { type: 'integer', default: 1 } } },
};
app.get(
'/inventory/:variantId/availability',
{ schema: availabilitySchema },
async (request, reply) => {
const { variantId } = parseJson(variantParamSchema, request.params);
const { quantity } = parseJson(availabilityQuerySchema, request.query);
const availability = await inventory.checkAvailability(variantId, quantity);
return reply.send(availability);
},
);
app.put('/inventory/:variantId/stock', async (request, reply) => {
const setStockSchema: FastifySchema = {
tags: ['Inventory'],
summary: 'Set available stock (admin)',
params: {
type: 'object',
required: ['variantId'],
properties: { variantId: { type: 'string', format: 'uuid' } },
},
body: {
type: 'object',
required: ['quantity'],
properties: { quantity: { type: 'integer', minimum: 0 } },
},
response: { 401: errorSchema, 403: errorSchema },
};
app.put('/inventory/:variantId/stock', { schema: setStockSchema }, async (request, reply) => {
const user = await deps.authenticate(request);
requireRole(user, 'admin');
const { variantId } = parseJson(variantParamSchema, request.params);
@@ -59,47 +90,127 @@ export async function registerInventoryRoutes(
}
});
app.post('/inventory/:variantId/reservations', async (request, reply) => {
const user = await deps.authenticate(request);
requireRole(user, 'admin');
const { variantId } = parseJson(variantParamSchema, request.params);
const { quantity } = parseJson(stockCommandBodySchema, request.body);
try {
const item = await inventory.reserve({ variantId, quantity });
return reply.code(201).send(serializeStockItem(item));
} catch (error) {
throw mapInventoryError(error);
}
});
const reserveSchema: FastifySchema = {
tags: ['Inventory'],
summary: 'Reserve stock (admin)',
params: {
type: 'object',
required: ['variantId'],
properties: { variantId: { type: 'string', format: 'uuid' } },
},
body: {
type: 'object',
required: ['quantity'],
properties: { quantity: { type: 'integer', minimum: 1 } },
},
response: { 401: errorSchema, 403: errorSchema },
};
app.post(
'/inventory/:variantId/reservations',
{ schema: reserveSchema },
async (request, reply) => {
const user = await deps.authenticate(request);
requireRole(user, 'admin');
const { variantId } = parseJson(variantParamSchema, request.params);
const { quantity } = parseJson(stockCommandBodySchema, request.body);
try {
const item = await inventory.reserve({ variantId, quantity });
return reply.code(201).send(serializeStockItem(item));
} catch (error) {
throw mapInventoryError(error);
}
},
);
app.post('/inventory/:variantId/reservations/release', async (request, reply) => {
const user = await deps.authenticate(request);
requireRole(user, 'admin');
const { variantId } = parseJson(variantParamSchema, request.params);
const { quantity } = parseJson(stockCommandBodySchema, request.body);
try {
const item = await inventory.release({ variantId, quantity });
return reply.send(serializeStockItem(item));
} catch (error) {
throw mapInventoryError(error);
}
});
const releaseSchema: FastifySchema = {
tags: ['Inventory'],
summary: 'Release reservation (admin)',
params: {
type: 'object',
required: ['variantId'],
properties: { variantId: { type: 'string', format: 'uuid' } },
},
body: {
type: 'object',
required: ['quantity'],
properties: { quantity: { type: 'integer', minimum: 1 } },
},
response: { 401: errorSchema, 403: errorSchema },
};
app.post(
'/inventory/:variantId/reservations/release',
{ schema: releaseSchema },
async (request, reply) => {
const user = await deps.authenticate(request);
requireRole(user, 'admin');
const { variantId } = parseJson(variantParamSchema, request.params);
const { quantity } = parseJson(stockCommandBodySchema, request.body);
try {
const item = await inventory.release({ variantId, quantity });
return reply.send(serializeStockItem(item));
} catch (error) {
throw mapInventoryError(error);
}
},
);
app.post('/inventory/:variantId/reservations/confirm', async (request, reply) => {
const user = await deps.authenticate(request);
requireRole(user, 'admin');
const { variantId } = parseJson(variantParamSchema, request.params);
const { quantity } = parseJson(stockCommandBodySchema, request.body);
try {
const item = await inventory.confirm({ variantId, quantity });
return reply.send(serializeStockItem(item));
} catch (error) {
throw mapInventoryError(error);
}
});
const confirmSchema: FastifySchema = {
tags: ['Inventory'],
summary: 'Confirm reservation (admin)',
params: {
type: 'object',
required: ['variantId'],
properties: { variantId: { type: 'string', format: 'uuid' } },
},
body: {
type: 'object',
required: ['quantity'],
properties: { quantity: { type: 'integer', minimum: 1 } },
},
response: { 401: errorSchema, 403: errorSchema },
};
app.post(
'/inventory/:variantId/reservations/confirm',
{ schema: confirmSchema },
async (request, reply) => {
const user = await deps.authenticate(request);
requireRole(user, 'admin');
const { variantId } = parseJson(variantParamSchema, request.params);
const { quantity } = parseJson(stockCommandBodySchema, request.body);
try {
const item = await inventory.confirm({ variantId, quantity });
return reply.send(serializeStockItem(item));
} catch (error) {
throw mapInventoryError(error);
}
},
);
// POST /inventory/bulk-adjust — atomic bulk stock adjustment
app.post('/inventory/bulk-adjust', async (request, reply) => {
const bulkSchema: FastifySchema = {
tags: ['Inventory'],
summary: 'Bulk stock adjustment (admin)',
description: 'Ajuste atómico de stock para múltiples variantes en una transacción.',
body: {
type: 'object',
required: ['items'],
properties: {
items: {
type: 'array',
items: {
type: 'object',
required: ['variantId', 'quantity'],
properties: {
variantId: { type: 'string', format: 'uuid' },
quantity: { type: 'integer', minimum: 0 },
},
},
},
},
},
response: { 401: errorSchema, 403: errorSchema },
};
app.post('/inventory/bulk-adjust', { schema: bulkSchema }, async (request, reply) => {
const user = await deps.authenticate(request);
requireRole(user, 'admin');
const { items } = parseJson(bulkAdjustBodySchema, request.body);

View File

@@ -5,6 +5,11 @@ import { PgInventoryRepository } from './infrastructure/pg-inventory-repository.
export { registerInventoryRoutes, type InventoryRoutesDeps } from './api/inventory.routes.js';
export { InventoryService } from './application/inventory-service.js';
export {
InsufficientReservedStockError,
InsufficientStockError,
InvalidStockQuantityError,
} from './domain/errors.js';
export type {
InventoryRepository,
InventoryService as InventoryServicePort,