feat(POS-002): completed feature

This commit is contained in:
chattie
2026-08-21 21:55:43 +02:00
parent c68cb25ed6
commit 0a1d32ecc3
33 changed files with 1169 additions and 107 deletions

View File

@@ -13,6 +13,7 @@ import {
InvalidStockQuantityError,
} from '../domain/errors.js';
import type { SetAvailableStockCommand, StockItem } from '../domain/stock.js';
import { DEFAULT_STORE_ID } from '../domain/stock.js';
import { PgInventoryRepository } from '../infrastructure/pg-inventory-repository.js';
export interface InventoryRoutesDeps {
@@ -23,9 +24,16 @@ export interface InventoryRoutesDeps {
const variantParamSchema = z.object({ variantId: z.uuid() });
const availabilityQuerySchema = z.object({
quantity: z.coerce.number().int().positive().default(1),
storeId: z.uuid().optional(),
});
const stockBodySchema = z.object({
quantity: z.number().int().min(0),
storeId: z.uuid().optional(),
});
const stockCommandBodySchema = z.object({
quantity: z.number().int().positive(),
storeId: z.uuid().optional(),
});
const stockBodySchema = z.object({ quantity: z.number().int().min(0) });
const stockCommandBodySchema = z.object({ quantity: z.number().int().positive() });
const bulkAdjustItemSchema = z.object({
variantId: z.uuid(),
@@ -49,15 +57,25 @@ export async function registerInventoryRoutes(
required: ['variantId'],
properties: { variantId: { type: 'string', format: 'uuid' } },
},
querystring: { type: 'object', properties: { quantity: { type: 'integer', default: 1 } } },
querystring: {
type: 'object',
properties: {
quantity: { type: 'integer', default: 1 },
storeId: { type: 'string', format: 'uuid' },
},
},
};
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);
const { quantity, storeId } = parseJson(availabilityQuerySchema, request.query);
const availability = await inventory.checkAvailability(
variantId,
storeId ?? DEFAULT_STORE_ID,
quantity,
);
return reply.send(availability);
},
);
@@ -81,9 +99,13 @@ export async function registerInventoryRoutes(
const user = await deps.authenticate(request);
requireRole(user, 'admin');
const { variantId } = parseJson(variantParamSchema, request.params);
const { quantity } = parseJson(stockBodySchema, request.body);
const { quantity, storeId } = parseJson(stockBodySchema, request.body);
try {
const item = await inventory.setAvailable({ variantId, quantity });
const item = await inventory.setAvailable({
variantId,
storeId: storeId ?? DEFAULT_STORE_ID,
quantity,
});
return reply.send(serializeStockItem(item));
} catch (error) {
throw mapInventoryError(error);
@@ -112,9 +134,13 @@ export async function registerInventoryRoutes(
const user = await deps.authenticate(request);
requireRole(user, 'admin');
const { variantId } = parseJson(variantParamSchema, request.params);
const { quantity } = parseJson(stockCommandBodySchema, request.body);
const { quantity, storeId } = parseJson(stockCommandBodySchema, request.body);
try {
const item = await inventory.reserve({ variantId, quantity });
const item = await inventory.reserve({
variantId,
storeId: storeId ?? DEFAULT_STORE_ID,
quantity,
});
return reply.code(201).send(serializeStockItem(item));
} catch (error) {
throw mapInventoryError(error);
@@ -144,9 +170,13 @@ export async function registerInventoryRoutes(
const user = await deps.authenticate(request);
requireRole(user, 'admin');
const { variantId } = parseJson(variantParamSchema, request.params);
const { quantity } = parseJson(stockCommandBodySchema, request.body);
const { quantity, storeId } = parseJson(stockCommandBodySchema, request.body);
try {
const item = await inventory.release({ variantId, quantity });
const item = await inventory.release({
variantId,
storeId: storeId ?? DEFAULT_STORE_ID,
quantity,
});
return reply.send(serializeStockItem(item));
} catch (error) {
throw mapInventoryError(error);
@@ -176,9 +206,13 @@ export async function registerInventoryRoutes(
const user = await deps.authenticate(request);
requireRole(user, 'admin');
const { variantId } = parseJson(variantParamSchema, request.params);
const { quantity } = parseJson(stockCommandBodySchema, request.body);
const { quantity, storeId } = parseJson(stockCommandBodySchema, request.body);
try {
const item = await inventory.confirm({ variantId, quantity });
const item = await inventory.confirm({
variantId,
storeId: storeId ?? DEFAULT_STORE_ID,
quantity,
});
return reply.send(serializeStockItem(item));
} catch (error) {
throw mapInventoryError(error);
@@ -223,6 +257,7 @@ export async function registerInventoryRoutes(
for (const item of items) {
const command: SetAvailableStockCommand = {
variantId: item.variantId,
storeId: DEFAULT_STORE_ID,
quantity: item.quantity,
};
@@ -230,6 +265,7 @@ export async function registerInventoryRoutes(
const result = await client.query<{
id: string;
variant_id: string;
store_id: string;
available: number;
reserved: number;
sold: number;
@@ -237,19 +273,19 @@ export async function registerInventoryRoutes(
created_at: Date;
updated_at: Date;
}>(
`INSERT INTO inventory_stock (variant_id, available)
VALUES ($1, $2)
ON CONFLICT (variant_id) DO UPDATE
`INSERT INTO inventory_stock (variant_id, store_id, available)
VALUES ($1, $2, $3)
ON CONFLICT (variant_id, store_id) DO UPDATE
SET available = EXCLUDED.available, updated_at = now()
RETURNING *`,
[command.variantId, command.quantity],
[command.variantId, command.storeId, command.quantity],
);
// Log the movement
await client.query(
`INSERT INTO inventory_movements (variant_id, operation, quantity)
VALUES ($1, $2, $3)`,
[command.variantId, 'bulk_adjust', command.quantity],
`INSERT INTO inventory_movements (variant_id, store_id, operation, quantity)
VALUES ($1, $2, $3, $4)`,
[command.variantId, command.storeId, 'bulk_adjust', command.quantity],
);
const row = result.rows[0];
@@ -259,6 +295,7 @@ export async function registerInventoryRoutes(
results.push({
id: row.id,
variantId: row.variant_id,
storeId: row.store_id,
available: row.available,
reserved: row.reserved,
sold: row.sold,