feat(frontend): Billing > Consumo (platform) + Sistema > Configurações — zera "em breve"
GET /billing/consumo agrega o mesmo uso bruto de /reports/consumo (tenant), só que em loop por todos os tenants — nunca dinheiro, só quantidade (dinheiro é Billing > Relatórios, já existia). GET /platform/system-config: "Sistema > Configurações" nunca teve escopo definido na especificação. Decisão desta implementação: painel somente leitura das flags de segurança/infra que já existem como variável de ambiente (DIALER_SIMULATION/ ALLOW_REAL_OUTBOUND_CALLS, ESL configurado, storage provider, NODE_ENV) — nunca editável por aqui, mudar exige editar o .env e reiniciar o serviço. Nunca expõe segredo nenhum. Com isto, todo item dos menus Platform e Tenant tem uma tela real por trás — zero "em breve" restando em nav-data.ts nos dois lados. Testado ponta a ponta: /billing/consumo batendo com os mesmos números já vistos em Relatórios > Consumo/Quotas, /platform/system-config confirmado mostrando o valor real do .env desta VM. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BFaBaBSQGhyXGEgtTYZGV8
This commit is contained in:
67
apps/api/src/billing/billing-consumo.controller.ts
Normal file
67
apps/api/src/billing/billing-consumo.controller.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { Controller, ForbiddenException, Get, UseGuards } from "@nestjs/common";
|
||||
import { getPrismaClient, withTenantContext } from "@b2bcall/database";
|
||||
import { isPlatformUser, type AccessTokenClaims } from "@b2bcall/auth";
|
||||
import { JwtAuthGuard } from "../common/guards/jwt-auth.guard";
|
||||
import { PermissionGuard } from "../common/guards/permission.guard";
|
||||
import { RequirePermission } from "../common/decorators/require-permission.decorator";
|
||||
import { CurrentUser } from "../common/decorators/current-user.decorator";
|
||||
|
||||
/**
|
||||
* "Billing > Consumo" (agente.md secao 169) — a mesma agregação de
|
||||
* `/reports/consumo` (tenant, mês corrente), só que em TODOS os tenants
|
||||
* de uma vez. Nunca calcula valor em dinheiro (isso é o RatingEngine/
|
||||
* BillingStatement — Billing > Relatórios), só a quantidade bruta dos 2
|
||||
* ledgers imutáveis. Diferente de Clientes > Quotas: aqui é "quanto cada
|
||||
* tenant consumiu", lá é "quanto sobra até o limite do plano".
|
||||
*/
|
||||
@UseGuards(JwtAuthGuard, PermissionGuard)
|
||||
@Controller("billing/consumo")
|
||||
export class BillingConsumoController {
|
||||
@RequirePermission("billing.view")
|
||||
@Get()
|
||||
async list(@CurrentUser() user: AccessTokenClaims): Promise<Record<string, unknown>[]> {
|
||||
if (!(await isPlatformUser(user.sub))) {
|
||||
throw new ForbiddenException("So' um usuario com role de plataforma pode ver consumo de todos os tenants");
|
||||
}
|
||||
const prisma = getPrismaClient();
|
||||
const now = new Date();
|
||||
const monthStart = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1));
|
||||
|
||||
const tenants = await prisma.tenant.findMany({ where: { deletedAt: null }, orderBy: { legalName: "asc" } });
|
||||
|
||||
return Promise.all(
|
||||
tenants.map(async (tenant) => {
|
||||
const [usageByMeter, aiUsageByType] = await withTenantContext(prisma, tenant.id, (tx) =>
|
||||
Promise.all([
|
||||
tx.usageEvent.groupBy({ by: ["meter"], where: { tenantId: tenant.id, occurredAt: { gte: monthStart } }, _sum: { quantity: true } }),
|
||||
tx.aIUsageRecord.groupBy({ by: ["type"], where: { tenantId: tenant.id, occurredAt: { gte: monthStart } }, _sum: { quantity: true } }),
|
||||
]),
|
||||
);
|
||||
|
||||
const usage: Record<string, number> = {};
|
||||
for (const row of usageByMeter) usage[row.meter] = row._sum.quantity ?? 0;
|
||||
const aiUsage: Record<string, number> = {};
|
||||
for (const row of aiUsageByType) aiUsage[row.type] = row._sum.quantity ?? 0;
|
||||
|
||||
return {
|
||||
tenantId: tenant.id,
|
||||
legalName: tenant.legalName,
|
||||
usage: {
|
||||
callCount: usage["CALL_COUNT"] ?? 0,
|
||||
callSeconds: usage["CALL_SECONDS"] ?? 0,
|
||||
extensionActiveDays: usage["EXTENSION_ACTIVE_DAY"] ?? 0,
|
||||
agentActiveDays: usage["AGENT_ACTIVE_DAY"] ?? 0,
|
||||
trunkActiveDays: usage["TRUNK_ACTIVE_DAY"] ?? 0,
|
||||
recordingBytes: usage["RECORDING_BYTES"] ?? 0,
|
||||
},
|
||||
aiUsage: {
|
||||
transcriptionSeconds: aiUsage["AI_TRANSCRIPTION_SECONDS"] ?? 0,
|
||||
analysisRequests: aiUsage["AI_ANALYSIS_REQUEST"] ?? 0,
|
||||
inputTokens: aiUsage["AI_INPUT_TOKENS"] ?? 0,
|
||||
outputTokens: aiUsage["AI_OUTPUT_TOKENS"] ?? 0,
|
||||
},
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { PlanVersionsController } from "./plan-versions.controller";
|
||||
import { SubscriptionsController } from "./subscriptions.controller";
|
||||
import { BillingPeriodsController } from "./billing-periods.controller";
|
||||
import { BillingStatementsController } from "./billing-statements.controller";
|
||||
import { BillingConsumoController } from "./billing-consumo.controller";
|
||||
|
||||
@Module({
|
||||
controllers: [
|
||||
@@ -14,6 +15,7 @@ import { BillingStatementsController } from "./billing-statements.controller";
|
||||
SubscriptionsController,
|
||||
BillingPeriodsController,
|
||||
BillingStatementsController,
|
||||
BillingConsumoController,
|
||||
],
|
||||
})
|
||||
export class BillingModule {}
|
||||
|
||||
45
apps/api/src/platform/platform-system-config.controller.ts
Normal file
45
apps/api/src/platform/platform-system-config.controller.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { Controller, ForbiddenException, Get, UseGuards } from "@nestjs/common";
|
||||
import { isPlatformUser, type AccessTokenClaims } from "@b2bcall/auth";
|
||||
import { JwtAuthGuard } from "../common/guards/jwt-auth.guard";
|
||||
import { PermissionGuard } from "../common/guards/permission.guard";
|
||||
import { RequirePermission } from "../common/decorators/require-permission.decorator";
|
||||
import { CurrentUser } from "../common/decorators/current-user.decorator";
|
||||
|
||||
/**
|
||||
* "Sistema > Configurações" (agente.md secao 169) — nunca teve escopo
|
||||
* definido na especificação. Decisão desta implementação: painel
|
||||
* read-only das flags de segurança/infra que já existem como variável de
|
||||
* ambiente (`.env`, compartilhado por `apps/api` e os workers via
|
||||
* systemd/docker-compose — ver `infrastructure/systemd/README.md` e
|
||||
* `docker-compose.yml`), nunca editável por aqui — mudar exige editar o
|
||||
* `.env` e reiniciar o serviço (documentado, não um botão de UI que
|
||||
* fingiria aplicar na hora). Nunca expõe segredo nenhum (senha, chave,
|
||||
* connection string) — só booleans/enums que já são público conhecimento
|
||||
* de quem administra a infraestrutura.
|
||||
*/
|
||||
@UseGuards(JwtAuthGuard, PermissionGuard)
|
||||
@Controller("platform/system-config")
|
||||
export class PlatformSystemConfigController {
|
||||
@RequirePermission("tenants.view")
|
||||
@Get()
|
||||
async get(@CurrentUser() user: AccessTokenClaims) {
|
||||
if (!(await isPlatformUser(user.sub))) {
|
||||
throw new ForbiddenException("So' um usuario com role de plataforma pode ver a configuracao do sistema");
|
||||
}
|
||||
|
||||
const dialerSimulation = (process.env.DIALER_SIMULATION ?? "true") === "true";
|
||||
const allowRealOutboundCalls = (process.env.ALLOW_REAL_OUTBOUND_CALLS ?? "false") === "true";
|
||||
|
||||
return {
|
||||
nodeEnv: process.env.NODE_ENV ?? "development",
|
||||
dialerSimulation,
|
||||
allowRealOutboundCalls,
|
||||
// As DUAS precisam estar explicitamente ligadas (secao 186) — nunca
|
||||
// basta uma pra originar PSTN de verdade.
|
||||
realOutboundCallsActive: !dialerSimulation && allowRealOutboundCalls,
|
||||
storageProvider: process.env.STORAGE_PROVIDER ?? "local",
|
||||
eslConfigured: Boolean(process.env.ESL_HOST) && Boolean(process.env.ESL_PASSWORD),
|
||||
corsOrigin: process.env.CORS_ORIGIN ?? null,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { PlatformRolesController } from "./platform-roles.controller";
|
||||
import { PlatformQuotasController } from "./platform-quotas.controller";
|
||||
import { PlatformFreeswitchController } from "./platform-freeswitch.controller";
|
||||
import { PlatformAiUsageController } from "./platform-ai-usage.controller";
|
||||
import { PlatformSystemConfigController } from "./platform-system-config.controller";
|
||||
|
||||
@Module({
|
||||
controllers: [
|
||||
@@ -18,6 +19,7 @@ import { PlatformAiUsageController } from "./platform-ai-usage.controller";
|
||||
PlatformQuotasController,
|
||||
PlatformFreeswitchController,
|
||||
PlatformAiUsageController,
|
||||
PlatformSystemConfigController,
|
||||
],
|
||||
})
|
||||
export class PlatformModule {}
|
||||
|
||||
Reference in New Issue
Block a user