it0/packages/services/ops-service/src/interfaces/rest/controllers/standing-order.controller.ts

164 lines
6.6 KiB
TypeScript

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);
}
}