import { Body, Controller, Get, Param, Post, Query, Req, UseGuards } from '@nestjs/common';
import type { z } from 'zod';
import { AuthGuard } from '../../../common/auth/auth.guard';
import { OrganizationContextGuard } from '../../../common/auth/organization-context.guard';
import { LocationContextGuard, type LocationScopedRequest } from '../../../common/auth/location-context.guard';
import { PermissionGuard } from '../../../common/auth/permission.guard';
import { RequirePermissions } from '../../../common/auth/permissions.decorator';
import { InventorySetupService } from '../application/inventory-setup.service';
import { StockMovementService } from '../application/stock-movement.service';
import { WasteService } from '../application/waste.service';
import { StockTransferService } from '../application/stock-transfer.service';
import { InventoryCountService } from '../application/inventory-count.service';
import { InventoryReadModelService } from '../read-models/inventory-read-model.service';
import { StockProjectionDiagnosticService } from '../application/stock-projection-diagnostic.service';
import { BusinessDateService } from '../../../common/time/business-date.service';
import { ZodValidationPipe } from '../../../common/validation/zod-validation.pipe';
import { inventoryAdjustmentSchema,inventoryWasteSchema,inventoryTransferSchema,inventoryCountPostSchema,inventoryTrustBudgetPolicySchema,inventoryTrustConsumptionSchema } from '../../../common/validation/critical-write.schemas';
import { InventoryTrustBudgetService } from '../application/inventory-trust-budget.service';
import { inventoryCountCreateSchema,inventoryCountLineSchema,inventoryLocationConfigSchema,inventoryReorderRuleSchema,inventoryStockItemSchema,inventoryStorageSchema } from '../../../common/validation/critical-write.schemas';
type InventoryStorageBody = z.infer<typeof inventoryStorageSchema>;
type InventoryStockItemBody = z.infer<typeof inventoryStockItemSchema>;
type InventoryLocationConfigBody = z.infer<typeof inventoryLocationConfigSchema>;
type InventoryReorderRuleBody = z.infer<typeof inventoryReorderRuleSchema>;
type InventoryAdjustmentBody = z.infer<typeof inventoryAdjustmentSchema>;
type InventoryWasteBody = z.infer<typeof inventoryWasteSchema>;
type InventoryTransferBody = z.infer<typeof inventoryTransferSchema>;
type InventoryCountCreateBody = z.infer<typeof inventoryCountCreateSchema>;
type InventoryCountLineBody = z.infer<typeof inventoryCountLineSchema>;
type InventoryCountPostBody = z.infer<typeof inventoryCountPostSchema>;
type InventoryTrustBudgetPolicyBody = z.infer<typeof inventoryTrustBudgetPolicySchema>;
type InventoryTrustConsumptionBody = z.infer<typeof inventoryTrustConsumptionSchema>;
@Controller('/inventory')
@UseGuards(AuthGuard, OrganizationContextGuard, LocationContextGuard, PermissionGuard)
export class InventoryController {
constructor(
private readonly setup: InventorySetupService,
private readonly movementsService: StockMovementService,
private readonly waste: WasteService,
private readonly transfers: StockTransferService,
private readonly counts: InventoryCountService,
private readonly readModel: InventoryReadModelService,
private readonly diagnostics:StockProjectionDiagnosticService,
private readonly businessDates:BusinessDateService,
private readonly trustBudget:InventoryTrustBudgetService,
) {}
@Get('/overview')
@RequirePermissions('inventory.read')
async overview(@Req() req:LocationScopedRequest) {
const businessDate=await this.businessDates.forLocation({
organizationId:req.organizationId!,locationId:req.locationId!,
});
return this.readModel.overview({
organizationId:req.organizationId!,locationId:req.locationId!,businessDate,
});
}
@Get('/stock')
@RequirePermissions('inventory.read')
stock(@Req() req: LocationScopedRequest) {
return this.readModel.stock({
organizationId: req.organizationId!,
locationId: req.locationId!,
});
}
@Get('/storage-locations')
@RequirePermissions('inventory.read')
storageLocations(@Req() req: LocationScopedRequest) {
return this.readModel.storageLocations({
organizationId: req.organizationId!,
locationId: req.locationId!,
});
}
@Get('/movements')
@RequirePermissions('inventory.read')
movements(
@Req() req: LocationScopedRequest,
@Query('stockItemId') stockItemId?: string,
) {
return this.readModel.movements({
organizationId: req.organizationId!,
locationId: req.locationId!,
stockItemId,
});
}
@Get('/projection-diagnostics')
@RequirePermissions('inventory.read')
projectionDiagnostics(@Req() req: LocationScopedRequest) {
return this.diagnostics.check({
organizationId: req.organizationId!,
locationId: req.locationId!,
});
}
@Post('/storage-locations')
@RequirePermissions('inventory.adjustments.create')
createStorage(@Req() req: LocationScopedRequest, @Body(new ZodValidationPipe(inventoryStorageSchema)) body:InventoryStorageBody) {
return this.setup.createStorageLocation({
organizationId: req.organizationId!,
locationId: req.locationId!,
name: body.name,
storageType: body.storageType,
sortOrder: body.sortOrder,
});
}
@Post('/stock-items')
@RequirePermissions('inventory.adjustments.create')
createItem(@Req() req: LocationScopedRequest, @Body(new ZodValidationPipe(inventoryStockItemSchema)) body:InventoryStockItemBody) {
return this.setup.createStockItem({
organizationId: req.organizationId!,
ingredientId: body.ingredientId,
name: body.name,
baseUnitCode: body.baseUnitCode,
});
}
@Post('/stock-items/:id/location')
@RequirePermissions('inventory.adjustments.create')
configureLocation(
@Param('id') id: string,
@Req() req: LocationScopedRequest,
@Body(new ZodValidationPipe(inventoryLocationConfigSchema)) body:InventoryLocationConfigBody,
) {
return this.setup.configureLocation({
stockItemId: id,
locationId: req.locationId!,
defaultStorageLocationId: body.defaultStorageLocationId,
isTracked: body.isTracked,
});
}
@Post('/adjustments')
@RequirePermissions('inventory.adjustments.create')
async adjustment(
@Req() req:LocationScopedRequest,
@Body(new ZodValidationPipe(inventoryAdjustmentSchema)) body:InventoryAdjustmentBody,
) {
const businessDate=await this.businessDates.forLocation({
organizationId:req.organizationId!,locationId:req.locationId!,
});
return this.movementsService.postManualAdjustment({
organizationId:req.organizationId!,locationId:req.locationId!,
stockItemId:body.stockItemId,storageLocationId:body.storageLocationId,
quantityDelta:body.quantityDelta,unitCode:body.unitCode,
normalizedQuantityDelta:body.normalizedQuantityDelta,
normalizedUnitCode:body.normalizedUnitCode,businessDate,
reason:body.reason,employeeId:body.employeeId,
});
}
@Post('/waste')
@RequirePermissions('inventory.waste.create')
async recordWaste(
@Req() req:LocationScopedRequest,
@Body(new ZodValidationPipe(inventoryWasteSchema)) body:InventoryWasteBody,
) {
const businessDate=await this.businessDates.forLocation({
organizationId:req.organizationId!,locationId:req.locationId!,
});
return this.waste.record({
organizationId:req.organizationId!,locationId:req.locationId!,
stockItemId:body.stockItemId,storageLocationId:body.storageLocationId,
quantity:body.quantity,unitCode:body.unitCode,
normalizedQuantity:body.normalizedQuantity,normalizedUnitCode:body.normalizedUnitCode,
reason:body.reason,businessDate,notes:body.notes,
sourceType:body.sourceType,sourceId:body.sourceId,employeeId:body.employeeId,
});
}
@Post('/transfers')
@RequirePermissions('inventory.transfer.create')
async transfer(
@Req() req:LocationScopedRequest,
@Body(new ZodValidationPipe(inventoryTransferSchema)) body:InventoryTransferBody,
) {
const businessDate=await this.businessDates.forLocation({
organizationId:req.organizationId!,locationId:req.locationId!,
});
return this.transfers.transfer({
organizationId:req.organizationId!,locationId:req.locationId!,
stockItemId:body.stockItemId,
fromStorageLocationId:body.fromStorageLocationId,toStorageLocationId:body.toStorageLocationId,
quantity:body.quantity,unitCode:body.unitCode,
normalizedQuantity:body.normalizedQuantity,normalizedUnitCode:body.normalizedUnitCode,
businessDate,reason:body.reason,employeeId:body.employeeId,
});
}
@Post('/reorder-rules')
@RequirePermissions('inventory.reorder.manage')
reorder(@Req() req: LocationScopedRequest, @Body(new ZodValidationPipe(inventoryReorderRuleSchema)) body:InventoryReorderRuleBody) {
return this.setup.setReorderRule({
organizationId: req.organizationId!,
locationId: req.locationId!,
stockItemId: body.stockItemId,
reorderPoint: body.reorderPoint,
targetStock: body.targetStock,
unitCode: body.unitCode,
});
}
@Post('/counts')
@RequirePermissions('inventory.count.start')
createCount(@Req() req: LocationScopedRequest, @Body(new ZodValidationPipe(inventoryCountCreateSchema)) body:InventoryCountCreateBody) {
return this.counts.create({
organizationId: req.organizationId!,
locationId: req.locationId!,
storageLocationId: body.storageLocationId,
scope: body.scope,
});
}
@Post('/counts/:id/start')
@RequirePermissions('inventory.count.start')
startCount(@Req() req: LocationScopedRequest, @Param('id') id: string, @Body(new ZodValidationPipe(inventoryCountPostSchema)) body:InventoryCountPostBody) {
return this.counts.start({
organizationId: req.organizationId!,
locationId: req.locationId!,
countId: id,
employeeId: body.employeeId,
});
}
@Get('/counts/:id')
@RequirePermissions('inventory.count.edit')
count(@Req() req: LocationScopedRequest, @Param('id') id: string) {
return this.counts.read({
organizationId: req.organizationId!,
locationId: req.locationId!,
countId: id,
});
}
@Post('/counts/:id/lines/:lineId')
@RequirePermissions('inventory.count.edit')
countLine(
@Req() req: LocationScopedRequest,
@Param('id') id: string,
@Param('lineId') lineId: string,
@Body(new ZodValidationPipe(inventoryCountLineSchema)) body:InventoryCountLineBody,
) {
return this.counts.setLine({
organizationId: req.organizationId!,
locationId: req.locationId!,
countId: id,
lineId,
countedQuantity: body.countedQuantity,
normalizedCountedQuantity: body.normalizedCountedQuantity,
notes: body.notes,
});
}
@Post('/counts/:id/review')
@RequirePermissions('inventory.count.edit')
reviewCount(@Req() req: LocationScopedRequest, @Param('id') id: string) {
return this.counts.review(req.organizationId!, req.locationId!, id);
}
@Post('/counts/:id/post')
@RequirePermissions('inventory.count.post')
async postCount(
@Req() req:LocationScopedRequest,@Param('id') id:string,
@Body(new ZodValidationPipe(inventoryCountPostSchema)) body:InventoryCountPostBody,
) {
const businessDate=await this.businessDates.forLocation({
organizationId:req.organizationId!,locationId:req.locationId!,
});
return this.counts.post({
organizationId:req.organizationId!,locationId:req.locationId!,countId:id,businessDate,employeeId:body.employeeId,
});
}
@Get('/trust-budget')
@RequirePermissions('inventory.trust_budget.read')
async trustBudgetStatus(@Req() req:LocationScopedRequest) {
const businessDate=await this.businessDates.forLocation({
organizationId:req.organizationId!,locationId:req.locationId!,
});
return this.trustBudget.usage({
organizationId:req.organizationId!,locationId:req.locationId!,businessDate,
});
}
@Post('/trust-budget/configure')
@RequirePermissions('inventory.trust_budget.manage')
configureTrustBudget(
@Req() req:LocationScopedRequest,
@Body(new ZodValidationPipe(inventoryTrustBudgetPolicySchema)) body:InventoryTrustBudgetPolicyBody,
) {
return this.trustBudget.configure({
organizationId:req.organizationId!,locationId:req.locationId!,
enabled:body.enabled,period:body.period,budgetMinor:body.budgetMinor,
currencyCode:body.currencyCode,overLimitBehavior:body.overLimitBehavior,
allowedReasonKeys:body.allowedReasonKeys,userId:req.auth?.userId,
});
}
@Post('/trust-budget/consume')
@RequirePermissions('inventory.trust_budget.use')
async consumeTrustBudget(
@Req() req:LocationScopedRequest,
@Body(new ZodValidationPipe(inventoryTrustConsumptionSchema)) body:InventoryTrustConsumptionBody,
) {
const businessDate=await this.businessDates.forLocation({
organizationId:req.organizationId!,locationId:req.locationId!,
});
return this.trustBudget.record({
organizationId:req.organizationId!,locationId:req.locationId!,
stockItemId:body.stockItemId,storageLocationId:body.storageLocationId,
quantity:body.quantity,unitCode:body.unitCode,
normalizedQuantity:body.normalizedQuantity,normalizedUnitCode:body.normalizedUnitCode,
businessDate,reasonKey:body.reasonKey,detailNote:body.detailNote,
employeeId:body.employeeId,
});
}
}
