feat(ADM-018): completed feature
This commit is contained in:
118
project/src/modules/inventory/api/inventory.routes.ts
Normal file
118
project/src/modules/inventory/api/inventory.routes.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import type { FastifyInstance } 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 { parseJson } from '../../../shared/http-input.js';
|
||||
import { InventoryService } from '../application/inventory-service.js';
|
||||
import {
|
||||
InsufficientReservedStockError,
|
||||
InsufficientStockError,
|
||||
InvalidStockQuantityError,
|
||||
} from '../domain/errors.js';
|
||||
import type { StockItem } from '../domain/stock.js';
|
||||
import { PgInventoryRepository } from '../infrastructure/pg-inventory-repository.js';
|
||||
|
||||
export interface InventoryRoutesDeps {
|
||||
pool: pg.Pool;
|
||||
authenticate: Authenticate;
|
||||
}
|
||||
|
||||
const variantParamSchema = z.object({ variantId: z.uuid() });
|
||||
const availabilityQuerySchema = z.object({
|
||||
quantity: z.coerce.number().int().positive().default(1),
|
||||
});
|
||||
const stockBodySchema = z.object({ quantity: z.number().int().min(0) });
|
||||
const stockCommandBodySchema = z.object({ quantity: z.number().int().positive() });
|
||||
|
||||
export async function registerInventoryRoutes(
|
||||
app: FastifyInstance,
|
||||
deps: InventoryRoutesDeps,
|
||||
): 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);
|
||||
});
|
||||
|
||||
app.put('/inventory/:variantId/stock', async (request, reply) => {
|
||||
const user = await deps.authenticate(request);
|
||||
requireRole(user, 'admin');
|
||||
const { variantId } = parseJson(variantParamSchema, request.params);
|
||||
const { quantity } = parseJson(stockBodySchema, request.body);
|
||||
try {
|
||||
const item = await inventory.setAvailable({ variantId, quantity });
|
||||
return reply.send(serializeStockItem(item));
|
||||
} catch (error) {
|
||||
throw mapInventoryError(error);
|
||||
}
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function mapInventoryError(error: unknown): Error {
|
||||
if (error instanceof InsufficientStockError) {
|
||||
return new AppError(409, 'INSUFFICIENT_STOCK', error.message);
|
||||
}
|
||||
if (error instanceof InsufficientReservedStockError) {
|
||||
return new AppError(409, 'INSUFFICIENT_RESERVED_STOCK', error.message);
|
||||
}
|
||||
if (error instanceof InvalidStockQuantityError) {
|
||||
return new AppError(422, 'INVALID_STOCK_QUANTITY', error.message);
|
||||
}
|
||||
return error instanceof Error ? error : new Error('Unknown inventory error');
|
||||
}
|
||||
|
||||
function serializeStockItem(item: StockItem) {
|
||||
return {
|
||||
id: item.id,
|
||||
variantId: item.variantId,
|
||||
available: item.available,
|
||||
reserved: item.reserved,
|
||||
sold: item.sold,
|
||||
incoming: item.incoming,
|
||||
createdAt: item.createdAt.toISOString(),
|
||||
updatedAt: item.updatedAt.toISOString(),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user