import { Controller, Get, Post, Put, Patch, Param, Body, NotFoundException, HttpException, HttpStatus } from '@nestjs/common'; import { StandingOrderRepository } from '../../../infrastructure/repositories/standing-order.repository'; import { StandingOrderExecutionRepository } from '../../../infrastructure/repositories/standing-order-execution.repository'; import { StandingOrder } from '../../../domain/entities/standing-order.entity'; import { TenantContextService } from '@it0/common'; import * as crypto from 'crypto'; @Controller('api/v1/ops/standing-orders') export class StandingOrderController { constructor( private readonly standingOrderRepository: StandingOrderRepository, private readonly executionRepository: StandingOrderExecutionRepository, ) {} @Get() async listOrders() { const orders = await this.standingOrderRepository.findAll(); return orders.map((o) => this.serializeOrder(o)); } /** * Maps incoming body from either web-admin simplified format or internal API format * to the StandingOrder entity fields. */ private mapBodyToOrder(order: StandingOrder, body: any): void { if (body.name !== undefined) order.name = body.name; if (body.description !== undefined) order.description = body.description; if (body.definedInSessionId !== undefined) order.definedInSessionId = body.definedInSessionId; if (body.escalationPolicyId !== undefined) order.escalationPolicyId = body.escalationPolicyId; // Trigger: accept both { trigger: { type, ... } } and { triggerConfig: { triggerType, ... } } if (body.trigger) { order.triggerType = body.trigger.type ?? 'manual'; order.trigger = body.trigger; } else if (body.triggerConfig) { const tc = body.triggerConfig; order.triggerType = tc.triggerType ?? 'manual'; order.trigger = { type: tc.triggerType ?? 'manual', cronExpression: tc.cronExpression, eventType: tc.eventType, thresholdCondition: tc.metric ? { metricType: tc.metric, operator: '>=', value: tc.threshold ?? 0, durationSeconds: 60 } : undefined, }; } // Targets: accept both { targets: { serverIds } } and { targetServers: string[] } if (body.targets) { order.targets = body.targets; } else if (body.targetServers) { order.targets = { serverIds: body.targetServers }; } // Agent instructions: accept nested, flat, or simplified format const ai = body.agentInstructions; order.agentPrompt = ai?.prompt ?? body.agentPrompt ?? body.agentInstruction ?? order.agentPrompt ?? ''; order.agentSkills = ai?.skills ?? body.agentSkills ?? order.agentSkills; order.runbookId = ai?.runbookId ?? body.runbookId ?? order.runbookId; order.maxRiskLevel = ai?.maxRiskLevel ?? body.maxRiskLevel ?? order.maxRiskLevel ?? 0; order.maxTurns = ai?.maxTurns ?? body.maxTurns ?? order.maxTurns ?? 20; order.maxBudgetUsd = ai?.maxBudgetUsd ?? body.maxBudgetUsd ?? body.maxBudget ?? order.maxBudgetUsd; // Decision boundary: accept full or simplified if (body.decisionBoundary) { order.decisionBoundary = body.decisionBoundary; } else if (body.escalateOnFailure !== undefined) { order.decisionBoundary = order.decisionBoundary ?? { allowedActions: [], escalateConditions: [], escalationRules: [] }; order.decisionBoundary.escalateConditions = body.escalateOnFailure ? ['on_failure'] : []; } } /** * Returns a serialized order with additional web-admin friendly aliases. */ private serializeOrder(order: StandingOrder) { return { ...order, // Web admin aliases triggerConfig: { triggerType: order.trigger?.type ?? order.triggerType, cronExpression: order.trigger?.cronExpression, eventType: order.trigger?.eventType, metric: order.trigger?.thresholdCondition?.metricType, threshold: order.trigger?.thresholdCondition?.value, }, targetServers: order.targets?.serverIds ?? [], agentInstruction: order.agentPrompt, maxBudget: order.maxBudgetUsd ?? 0, escalateOnFailure: order.decisionBoundary?.escalateConditions?.includes('on_failure') ?? false, }; } @Post() async createOrder(@Body() body: any) { // Quota enforcement const tenant = TenantContextService.getTenant(); if (tenant && tenant.maxStandingOrders !== -1) { const existing = await this.standingOrderRepository.findAll(); const activeCount = existing.filter((o) => o.status === 'active').length; if (activeCount >= tenant.maxStandingOrders) { throw new HttpException( { message: `Standing order quota exceeded (${activeCount}/${tenant.maxStandingOrders}). Upgrade your plan to create more.`, code: 'QUOTA_EXCEEDED' }, HttpStatus.TOO_MANY_REQUESTS, ); } } const order = new StandingOrder(); order.id = crypto.randomUUID(); order.tenantId = body.tenantId; order.status = 'active'; order.validFrom = body.validFrom ? new Date(body.validFrom) : new Date(); order.validUntil = body.validUntil ? new Date(body.validUntil) : undefined; order.createdBy = body.createdBy; order.createdAt = new Date(); order.updatedAt = new Date(); this.mapBodyToOrder(order, body); const saved = await this.standingOrderRepository.save(order); return this.serializeOrder(saved); } @Get(':id') async getOrder(@Param('id') id: string) { const order = await this.standingOrderRepository.findById(id); if (!order) { throw new NotFoundException(`Standing order ${id} not found`); } return this.serializeOrder(order); } @Put(':id') async updateOrder(@Param('id') id: string, @Body() body: any) { const order = await this.standingOrderRepository.findById(id); if (!order) { throw new NotFoundException(`Standing order ${id} not found`); } this.mapBodyToOrder(order, body); order.updatedAt = new Date(); const saved = await this.standingOrderRepository.save(order); return this.serializeOrder(saved); } @Patch(':id/status') async updateStatus(@Param('id') id: string, @Body() body: { status: string }) { const order = await this.standingOrderRepository.findById(id); if (!order) { throw new NotFoundException(`Standing order ${id} not found`); } order.status = body.status as StandingOrder['status']; order.updatedAt = new Date(); return this.standingOrderRepository.save(order); } @Get(':id/executions') async getExecutions(@Param('id') id: string) { const order = await this.standingOrderRepository.findById(id); if (!order) { throw new NotFoundException(`Standing order ${id} not found`); } return this.executionRepository.findByOrderId(id); } }