feat(F-048): completed feature
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user