import { Body, Controller, Get, Param, Post, Put, Query, Req, UseGuards } from '@nestjs/common';
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 { CostingConfigurationService } from '../application/costing-configuration.service';
import { CostAlertService } from '../application/cost-alert.service';
import { CostRecalculationWorker } from '../workers/cost-recalculation.worker';
import { ZodValidationPipe } from '../../../common/validation/zod-validation.pipe';
import { costingConfigSchema,costingRecalculateSchema } from '../../../common/validation/critical-write.schemas';
import { CostingReadModelService } from '../read-models/costing-read-model.service';
import type { z } from 'zod';
type CostingConfigBody = z.infer<typeof costingConfigSchema>;
type CostingRecalculateBody = z.infer<typeof costingRecalculateSchema>;
@Controller('/costing')
@UseGuards(AuthGuard, OrganizationContextGuard, LocationContextGuard, PermissionGuard)
export class CostingController {
constructor(
private readonly config: CostingConfigurationService,
private readonly alertsService: CostAlertService,
private readonly worker: CostRecalculationWorker,
private readonly readModel: CostingReadModelService,
) {}
@Get('/config')
@RequirePermissions('costing.read')
getConfig(@Req() req: LocationScopedRequest) {
return this.config.get({
organizationId: req.organizationId!,
locationId: req.locationId!,
});
}
@Put('/config')
@RequirePermissions('costing.manage')
putConfig(@Req() req: LocationScopedRequest, @Body(new ZodValidationPipe(costingConfigSchema)) body:CostingConfigBody) {
return this.config.put({
organizationId: req.organizationId!,
locationId: req.locationId!,
method: body.method,
currencyCode: body.currencyCode ?? 'EUR',
includeWasteFactor: body.includeWasteFactor ?? true,
targetFoodCostPercent: body.targetFoodCostPercent,
minimumGrossMarginPercent: body.minimumGrossMarginPercent,
userId: req.auth?.userId,
});
}
@Post('/recalculate')
@RequirePermissions('costing.recalculate')
recalculate(@Req() req: LocationScopedRequest, @Body(new ZodValidationPipe(costingRecalculateSchema)) body:CostingRecalculateBody) {
return this.worker.enqueue({
organizationId: req.organizationId!,
locationId: req.locationId!,
scopeType: body.scopeType ?? 'all',
scopeId: body.scopeId,
reason: body.reason ?? 'manual_recalculation',
});
}
@Post('/recalculate/run')
@RequirePermissions('costing.recalculate')
run() {
return this.worker.runBatch();
}
@Get('/overview')
@RequirePermissions('costing.read')
overview(@Req() req: LocationScopedRequest) {
return this.readModel.overview({
organizationId: req.organizationId!,
locationId: req.locationId!,
});
}
@Get('/alerts')
@RequirePermissions('costing.read')
alerts(@Req() req: LocationScopedRequest) {
return this.readModel.alerts({
organizationId: req.organizationId!,
locationId: req.locationId!,
});
}
@Post('/alerts/refresh')
@RequirePermissions('costing.recalculate')
refreshAlerts(@Req() req: LocationScopedRequest) {
return this.alertsService.refresh({
organizationId: req.organizationId!,
locationId: req.locationId!,
});
}
@Get('/recipes/:id')
@RequirePermissions('costing.read')
recipeCost(@Req() req: LocationScopedRequest, @Param('id') id: string) {
return this.readModel.recipeCost({
organizationId: req.organizationId!,
locationId: req.locationId!,
recipeId: id,
});
}
@Get('/recipes/:id/history')
@RequirePermissions('costing.read')
recipeHistory(@Req() req: LocationScopedRequest, @Param('id') id: string) {
return this.readModel.recipeHistory({
organizationId: req.organizationId!,
locationId: req.locationId!,
recipeId: id,
});
}
@Get('/products/:id/margin')
@RequirePermissions('costing.read')
productMargin(
@Req() req: LocationScopedRequest,
@Param('id') id: string,
@Query('channel') channel?: string,
) {
return this.readModel.productMargin({
organizationId: req.organizationId!,
locationId: req.locationId!,
productId: id,
channel: channel ?? 'pos',
});
}
@Get('/suppliers/:id/impact')
@RequirePermissions('costing.supplier_impact.read')
supplierImpact(
@Req() req: LocationScopedRequest,
@Param('id') id: string,
) {
return this.readModel.supplierImpact({
organizationId: req.organizationId!,
locationId: req.locationId!,
supplierId: id,
});
}
@Get('/modifiers/:id')
@RequirePermissions('costing.modifiers.read')
modifierProfitability(
@Req() req: LocationScopedRequest,
@Param('id') id: string,
) {
return this.readModel.modifierProfitability({
organizationId: req.organizationId!,
locationId: req.locationId!,
modifierId: id,
});
}
}
