feat(platform): Billing > Fechamentos e Relatórios
GET /billing/periods e /billing/statements só serviam o próprio tenant do JWT — sem uso pra um platform admin escolhendo um tenant arbitrário. Adicionado GET .../by-tenant/:tenantId nos dois (mesmo padrão já usado em Subscriptions), e GET /billing/statements/:id ganhou um ?tenantId= opcional só aceito de quem tem role de plataforma. Frontend: /platform/billing/fechamentos (fecha/reabre período por tenant, seletor via querystring pra não duplicar rota) e /relatorios (statements por tenant, detalhe com itens por categoria). Bug real achado testando o fluxo: fechar um período de 01/08 a 31/08 mostrava "31 de jul." a "30 de ago." — meia-noite UTC de uma data-only vira o dia anterior no timezone local do servidor. Corrigido com formatDateUTC novo, usado só em fronteiras de calendário (não em timestamps de verdade, que continuam com formatDate local). Testado ponta a ponta contra a API real: período fechado, statement gerado (R$ 0,00 honesto — Acme sem assinatura/price book ainda), detalhe correto. Smoke test nas 19 telas do tenant + 8 telas platform, todas 200. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BFaBaBSQGhyXGEgtTYZGV8
This commit is contained in:
@@ -37,3 +37,58 @@ export interface RateDeck {
|
||||
updatedAt: string;
|
||||
entries: RateDeckEntry[];
|
||||
}
|
||||
|
||||
export interface BillingPeriod {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
periodStart: string;
|
||||
periodEnd: string;
|
||||
status: "OPEN" | "CALCULATING" | "READY" | "CLOSED" | "REOPENED";
|
||||
closedAt: string | null;
|
||||
reopenedAt: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export const BILLING_PERIOD_STATUS_LABELS: Record<string, string> = {
|
||||
OPEN: "Aberto",
|
||||
CALCULATING: "Calculando",
|
||||
READY: "Pronto",
|
||||
CLOSED: "Fechado",
|
||||
REOPENED: "Reaberto",
|
||||
};
|
||||
|
||||
export const BILLING_STATEMENT_CATEGORY_LABELS: Record<string, string> = {
|
||||
PLAN_BASE: "Assinatura base",
|
||||
EXTENSIONS: "Ramais",
|
||||
AGENTS: "Agentes",
|
||||
TRUNKS: "Troncos",
|
||||
CALLS: "Chamadas",
|
||||
MINUTES: "Minutos",
|
||||
AI_TRANSCRIPTION: "IA — transcrição",
|
||||
AI_ANALYSIS: "IA — análise",
|
||||
AI_TOKENS: "IA — tokens",
|
||||
STORAGE: "Armazenamento",
|
||||
ADJUSTMENT: "Ajuste",
|
||||
};
|
||||
|
||||
export interface BillingStatementItem {
|
||||
id: string;
|
||||
category: string;
|
||||
description: string;
|
||||
quantity: number | null;
|
||||
unitPrice: number | null;
|
||||
amount: number;
|
||||
}
|
||||
|
||||
export interface BillingStatement {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
billingPeriodId: string;
|
||||
currency: string;
|
||||
subtotal: number;
|
||||
adjustments: number;
|
||||
total: number;
|
||||
generatedAt: string;
|
||||
billingPeriod: BillingPeriod;
|
||||
items?: BillingStatementItem[];
|
||||
}
|
||||
|
||||
@@ -37,6 +37,15 @@ export function formatDate(iso: string): string {
|
||||
return new Intl.DateTimeFormat("pt-BR", { day: "2-digit", month: "short", year: "numeric" }).format(new Date(iso));
|
||||
}
|
||||
|
||||
/** Como `formatDate`, mas em UTC — pra fronteiras de calendário (início/fim
|
||||
* de período de billing, por exemplo) que nunca deveriam deslizar um dia
|
||||
* pra trás por causa do timezone local do servidor renderizando a página.
|
||||
* Achado real: fechar um período de 01/08 a 31/08 mostrava "31 de jul." a
|
||||
* "30 de ago." porque meia-noite UTC vira o dia anterior em UTC-3. */
|
||||
export function formatDateUTC(iso: string): string {
|
||||
return new Intl.DateTimeFormat("pt-BR", { day: "2-digit", month: "short", year: "numeric", timeZone: "UTC" }).format(new Date(iso));
|
||||
}
|
||||
|
||||
export function formatDateTime(iso: string): string {
|
||||
return new Intl.DateTimeFormat("pt-BR", {
|
||||
day: "2-digit",
|
||||
|
||||
Reference in New Issue
Block a user