feat(ADM-018): completed feature

This commit is contained in:
chattie
2026-08-17 22:23:10 +02:00
parent cf1c69fc8b
commit d595b4871f
871 changed files with 47411 additions and 281 deletions

View 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(),
};
}