diff --git a/TODO.md b/TODO.md index 38ac8da..d4f4a0e 100644 --- a/TODO.md +++ b/TODO.md @@ -589,8 +589,69 @@ docs/QUALITY_SCORECARDS.md (mesma restrição de todo o módulo de IA); uma `QualityEvaluation` completando de verdade (só via dead-letter, sem rede real) -## PHASE 22+ — ver `agente.md` seções 120 em diante (Usage Metering -completo, Billing, Frontend, Security, Tests) +## PHASE 22 — Usage Metering / Billing (agente.md secao 120-139) — ver +docs/BILLING.md +- [x] Migration `billing` (`PlanVersion`/`TenantSubscription`/`PriceBook`+ + `PriceBookItem`/`RateDeck`+`RateDeckEntry`/`UsageEvent`/ + `RatedUsageItem`/`BillingPeriod`/`BillingStatement`+ + `BillingStatementItem`, RLS nas tenant-scoped) e `packages/billing` + (`RatingEngine` puro: longest-prefix match, rating por destino/ + fallback plano, prorateio de dias ativos, tokens/transcrição de IA, + armazenamento) já existiam de uma sessão anterior interrompida — + só a orquestração (I/O) e os endpoints faltavam +- [x] `apps/api/src/billing/billing-engine.service.ts`: + `closeBillingPeriod`/`reopenBillingPeriod` — lê os 2 ledgers + imutáveis (`UsageEvent`+`AIUsageRecord`) ainda não tarifados + (`ratedUsageItems: { none: {} }`), grava 1 `RatedUsageItem` por + evento (nunca agrega antes de ratear), soma `PLAN_BASE` da + `TenantSubscription` ativa, agrega por categoria em + `BillingStatementItem`. Fechamento imutável (secao 137): `CLOSED` + de novo é 409, só reopen explícito (audit trail) permite recalcular +- [x] Escritores do ledger `UsageEvent`: `CALL_SECONDS` em + `apps/freeswitch-events/src/cdr.ts::finalizeCall` (mesma transação + do CDR); `EXTENSION_ACTIVE_DAY`/`AGENT_ACTIVE_DAY`/ + `TRUNK_ACTIVE_DAY` em `apps/api/src/billing/active-day-sweep.ts` + (boot + hora em hora, idempotente por dia) +- [x] Controllers: `PriceBooksController`/`RateDecksController`/ + `PlanVersionsController` (catálogo global, `pricing.manage` + + `isPlatformUser`), `SubscriptionsController`/ + `BillingPeriodsController` (`billing.manage` + `isPlatformUser`, + `tenantId` explícito no body — ação de platform admin sobre um + tenant arbitrário), `BillingStatementsController` (`billing.view`, + sempre o próprio tenant do JWT) +- [x] **Bug real, achado no teste desta fase**: `reopenBillingPeriod` lia + o período sem tenant context — `billing_periods` tem FORCE RLS, + então a leitura nunca via a linha e "not found" virava 500 em vez + de 404. Corrigido exigindo `tenantId` explícito no reopen (igual ao + close) e lendo dentro de `withTenantContext`. +- [x] **Bug real, achado no teste desta fase**: `SubscriptionsController` + criava/lia `TenantSubscription` sem `withTenantContext` — RLS + rejeitava o create e o list sempre voltava vazio. Corrigido. +- [x] Teste unitário do `RatingEngine` (`packages/billing`, 17 casos, + `pnpm --filter @b2bcall/billing run test`) + testado ponta a ponta + contra a API real e Postgres real com RLS: período fechado com 8 + `RatedUsageItem`s (chamada, 3 dias de ramal ativo, transcrição, + análise, tokens de entrada/saída) e total batendo exatamente com o + cálculo manual; fechar 2x = 409; reopen + reclose reusa os itens já + tarifados; reopen com tenant errado = 404 (RLS isolando de + verdade); tenant admin sem role de plataforma barrado (403) de + fechar mas lista as próprias statements normalmente; + `runActiveDaySweep` chamado 2x no mesmo dia sem duplicar +- [ ] **Lacuna real, conhecida**: `Call.calledNumber` ainda não é + populado pelo CDR (PHASE 17) — `CALL_SECONDS` sempre usa o fallback + plano (`CALL_MINUTE`), nunca o `RateDeck` por destino real + (`longestPrefixMatch`/`rateCallByDestination` só testados + isoladamente, não ponta a ponta) +- [ ] `RECORDING_BYTES` usa o storage atual no momento do fechamento como + proxy do período inteiro (sem histórico de tamanho por dia) — + decisão documentada em docs/BILLING.md, não uma média ponderada +- [ ] Reajuste de preço no meio de um período aberto (2 vigências + sobrepostas de `PriceBookItem`/`RateDeckEntry`) nunca exercitado +- [ ] Sem UI de platform admin pra criar tenant/price book/rate deck + ainda (fase Frontend) — testado só via API direto + +## PHASE 23+ — ver `agente.md` seções 140 em diante (Frontend, Security, +Tests) --- diff --git a/apps/api/package.json b/apps/api/package.json index f85286b..12db8c4 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -11,6 +11,7 @@ "dependencies": { "@b2bcall/ai": "workspace:*", "@b2bcall/auth": "workspace:*", + "@b2bcall/billing": "workspace:*", "@b2bcall/database": "workspace:*", "@b2bcall/entitlements": "workspace:*", "@b2bcall/shared": "workspace:*", diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts index ed40c1b..db60ce3 100644 --- a/apps/api/src/app.module.ts +++ b/apps/api/src/app.module.ts @@ -17,6 +17,8 @@ import { ReportsModule } from "./reports/reports.module"; import { RecordingsModule } from "./recordings/recordings.module"; import { AIModule } from "./ai/ai.module"; import { QualityModule } from "./quality/quality.module"; +import { PlatformModule } from "./platform/platform.module"; +import { BillingModule } from "./billing/billing.module"; @Module({ imports: [ @@ -38,6 +40,8 @@ import { QualityModule } from "./quality/quality.module"; RecordingsModule, AIModule, QualityModule, + PlatformModule, + BillingModule, ], }) export class AppModule {} diff --git a/apps/api/src/auth/auth.controller.ts b/apps/api/src/auth/auth.controller.ts index 0bb05f1..ec09a89 100644 --- a/apps/api/src/auth/auth.controller.ts +++ b/apps/api/src/auth/auth.controller.ts @@ -1,7 +1,9 @@ -import { Body, Controller, Get, HttpCode, HttpStatus, Post, Req, UseGuards } from "@nestjs/common"; +import { Body, Controller, Get, HttpCode, HttpStatus, NotFoundException, Post, Req, UseGuards } from "@nestjs/common"; import type { FastifyRequest } from "fastify"; +import { getPrismaClient } from "@b2bcall/database"; import { changePassword, + isPlatformUser, listUserTenants, login, logout, @@ -44,6 +46,18 @@ export class AuthController { await logout(user.sessionId); } + @UseGuards(JwtAuthGuard) + @Get("me") + async me(@CurrentUser() user: AccessTokenClaims) { + const prisma = getPrismaClient(); + const dbUser = await prisma.user.findUnique({ + where: { id: user.sub }, + select: { id: true, email: true, name: true }, + }); + if (!dbUser) throw new NotFoundException(); + return { ...dbUser, isPlatformUser: await isPlatformUser(user.sub) }; + } + @UseGuards(JwtAuthGuard) @Get("tenants") async tenants(@CurrentUser() user: AccessTokenClaims) { diff --git a/apps/api/src/billing/active-day-sweep.ts b/apps/api/src/billing/active-day-sweep.ts new file mode 100644 index 0000000..166d5e2 --- /dev/null +++ b/apps/api/src/billing/active-day-sweep.ts @@ -0,0 +1,74 @@ +import { getPrismaClient, withTenantContext, type Prisma } from "@b2bcall/database"; +import { createLogger } from "@b2bcall/shared"; + +const logger = createLogger("b2bcall-api"); + +type Meter = "EXTENSION_ACTIVE_DAY" | "AGENT_ACTIVE_DAY" | "TRUNK_ACTIVE_DAY"; + +async function recordDailyMeter( + tx: Prisma.TransactionClient, + tenantId: string, + meter: Meter, + sourceType: string, + rows: Array<{ id: string }>, + todayStart: Date, + todayEnd: Date, +): Promise { + for (const row of rows) { + // Sem constraint unica no banco pra (tenantId, meter, sourceId, dia) — + // checagem explicita antes do insert. Corrida real possivel se a + // varredura rodar 2x em paralelo pro mesmo tenant (nao acontece hoje, + // um unico processo apps/api chama isso num setInterval sequencial), + // documentado como limitacao conhecida em docs/BILLING.md. + const exists = await tx.usageEvent.findFirst({ + where: { tenantId, meter, sourceId: row.id, occurredAt: { gte: todayStart, lt: todayEnd } }, + }); + if (exists) continue; + + await tx.usageEvent.create({ + data: { + tenantId, + meter, + quantity: 1, + unit: "day", + sourceType, + sourceId: row.id, + occurredAt: todayStart, + }, + }); + } +} + +/** + * "EXTENSION_ACTIVE_DAY"/"AGENT_ACTIVE_DAY"/"TRUNK_ACTIVE_DAY" (agente.md + * secao 131) — 1 `UsageEvent` por recurso ativo por dia, consumido pelo + * `BillingEngineService` no fechamento (`rateActiveDaysProrated`). Roda no + * boot + de hora em hora (mesmo padrão de `runRetentionSweep`) — idempotente + * dentro do mesmo dia (não duplica se já rodou hoje pra aquele recurso). + */ +export async function runActiveDaySweep(): Promise { + const prisma = getPrismaClient(); + const todayStart = new Date(); + todayStart.setUTCHours(0, 0, 0, 0); + const todayEnd = new Date(todayStart.getTime() + 24 * 60 * 60 * 1000); + + const tenants = await prisma.tenant.findMany({ where: { status: "ACTIVE" }, select: { id: true } }); + + for (const tenant of tenants) { + try { + await withTenantContext(prisma, tenant.id, async (tx) => { + const [extensions, agents, trunks] = await Promise.all([ + tx.extension.findMany({ where: { tenantId: tenant.id, deletedAt: null }, select: { id: true } }), + tx.agent.findMany({ where: { tenantId: tenant.id, deletedAt: null }, select: { id: true } }), + tx.trunk.findMany({ where: { tenantId: tenant.id, deletedAt: null }, select: { id: true } }), + ]); + + await recordDailyMeter(tx, tenant.id, "EXTENSION_ACTIVE_DAY", "extension", extensions, todayStart, todayEnd); + await recordDailyMeter(tx, tenant.id, "AGENT_ACTIVE_DAY", "agent", agents, todayStart, todayEnd); + await recordDailyMeter(tx, tenant.id, "TRUNK_ACTIVE_DAY", "trunk", trunks, todayStart, todayEnd); + }); + } catch (err) { + logger.error("falha na varredura de uso diario (billing)", { error: String(err), tenantId: tenant.id }); + } + } +} diff --git a/apps/api/src/billing/billing-engine.service.ts b/apps/api/src/billing/billing-engine.service.ts new file mode 100644 index 0000000..789f8d4 --- /dev/null +++ b/apps/api/src/billing/billing-engine.service.ts @@ -0,0 +1,330 @@ +import { BadRequestException, ConflictException, NotFoundException } from "@nestjs/common"; +import { getPrismaClient, withTenantContext, type Prisma, type PriceItemType } from "@b2bcall/database"; +import { + resolvePriceBookItem, + rateCallFlatFallback, + rateGenericUsage, + rateActiveDaysProrated, + rateTranscriptionSeconds, + rateRecordingBytes, + type PriceBookItemLike, +} from "@b2bcall/billing"; +import { recordAuditEvent } from "@b2bcall/auth"; + +const MS_PER_DAY = 24 * 60 * 60 * 1000; + +/** UsageMeter -> PriceItemType, pra métricas do tipo "N dias ativo" (agente.md + * secao 131) — preço mensal do item, prorateado por `rateActiveDaysProrated`. */ +const ACTIVE_DAY_METER_TO_PRICE_TYPE: Record = { + EXTENSION_ACTIVE_DAY: "EXTENSION_MONTH", + AGENT_ACTIVE_DAY: "AGENT_MONTH", + TRUNK_ACTIVE_DAY: "TRUNK_MONTH", +}; + +/** AIUsageType -> PriceItemType (secao 124/131 — nomes não batem 1:1, + * "SECONDS"/"REQUEST"/plural de token na origem viram "MINUTE"/"CALL"/ + * singular no catálogo de preço). */ +const AI_USAGE_TYPE_TO_PRICE_TYPE: Record = { + AI_TRANSCRIPTION_SECONDS: "AI_TRANSCRIPTION_MINUTE", + AI_ANALYSIS_REQUEST: "AI_ANALYSIS_CALL", + AI_INPUT_TOKENS: "AI_INPUT_TOKEN", + AI_OUTPUT_TOKENS: "AI_OUTPUT_TOKEN", +}; + +/** PriceItemType -> BillingStatementCategory, pra agrupar `RatedUsageItem`s + * na linha do statement (agente.md secao 136). */ +const PRICE_TYPE_TO_CATEGORY: Record = { + BASE_SUBSCRIPTION: "PLAN_BASE", + EXTENSION_MONTH: "EXTENSIONS", + AGENT_MONTH: "AGENTS", + TRUNK_MONTH: "TRUNKS", + CALL: "CALLS", + CALL_MINUTE: "MINUTES", + FIXED_MINUTE: "MINUTES", + MOBILE_MINUTE: "MINUTES", + INTERNATIONAL_MINUTE: "MINUTES", + AI_TRANSCRIPTION_MINUTE: "AI_TRANSCRIPTION", + AI_ANALYSIS_CALL: "AI_ANALYSIS", + AI_INPUT_TOKEN: "AI_TOKENS", + AI_OUTPUT_TOKEN: "AI_TOKENS", + RECORDING_GB_MONTH: "STORAGE", +}; + +const CATEGORY_LABEL: Record = { + PLAN_BASE: "Assinatura do plano", + EXTENSIONS: "Ramais ativos", + AGENTS: "Agentes ativos", + TRUNKS: "Troncos ativos", + CALLS: "Chamadas", + MINUTES: "Minutos de chamada", + AI_TRANSCRIPTION: "Transcricao (IA)", + AI_ANALYSIS: "Analise de chamada (IA)", + AI_TOKENS: "Tokens (IA)", + STORAGE: "Armazenamento de gravacoes", + ADJUSTMENT: "Ajuste", +}; + +/** + * RatingEngine é matemática pura (packages/billing, sem I/O — ver + * comentário em rating-engine.ts); este service faz a orquestração real + * (agente.md secao 130-137): resolve catálogos vigentes, lê os 2 ledgers + * imutáveis (`UsageEvent`+`AIUsageRecord`), grava `RatedUsageItem` (1 por + * evento — nunca agrega antes de ratear, "immutable usage ledger" secao + * 233) e fecha em `BillingStatement`/`BillingStatementItem` (agregado por + * categoria, o que o tenant efetivamente vê). + * + * **Lacuna real, conhecida**: `Call.calledNumber` ainda não é populado + * pelo CDR (ver TODO.md PHASE 17) — não dá pra fazer o longest-prefix + * match do RateDeck (secao 129) por destino real. `CALL_SECONDS` sempre + * usa `rateCallFlatFallback` (PriceBookItem `CALL_MINUTE`) por enquanto; + * `RateDeck`/`longestPrefixMatch` ficam cadastráveis e testados + * isoladamente (packages/billing tem teste unitário), só não são + * exercitados ponta a ponta até essa lacuna fechar. + */ +export async function closeBillingPeriod(opts: { + tenantId: string; + periodStart: Date; + periodEnd: Date; + userId: string; +}): Promise<{ periodId: string; statementId: string; total: number }> { + const { tenantId, periodStart, periodEnd, userId } = opts; + if (periodStart >= periodEnd) { + throw new BadRequestException("periodStart deve ser anterior a periodEnd"); + } + const daysInPeriod = (periodEnd.getTime() - periodStart.getTime()) / MS_PER_DAY; + const prisma = getPrismaClient(); + + const result = await withTenantContext(prisma, tenantId, async (tx) => { + const existing = await tx.billingPeriod.findUnique({ + where: { tenantId_periodStart_periodEnd: { tenantId, periodStart, periodEnd } }, + }); + if (existing?.status === "CLOSED") { + throw new ConflictException("Periodo ja fechado (secao 137) — reabra explicitamente antes de recalcular"); + } + if (existing?.status === "CALCULATING") { + throw new ConflictException("Fechamento ja em andamento para este periodo"); + } + + const period = existing + ? await tx.billingPeriod.update({ where: { id: existing.id }, data: { status: "CALCULATING" } }) + : await tx.billingPeriod.create({ data: { tenantId, periodStart, periodEnd, status: "CALCULATING" } }); + + const tenant = await tx.tenant.findUniqueOrThrow({ where: { id: tenantId } }); + const priceBook = await tx.priceBook.findFirst({ + where: tenant.priceBookId ? { id: tenant.priceBookId } : { isDefault: true }, + include: { items: true }, + }); + if (!priceBook) { + throw new ConflictException("Nenhum PriceBook configurado (nem default) para tarifar este tenant"); + } + + const priceItems: PriceBookItemLike[] = priceBook.items; + const callMinuteItem = resolvePriceBookItem(priceItems, "CALL_MINUTE", periodEnd); + + const [usageEvents, aiUsageRecords] = await Promise.all([ + tx.usageEvent.findMany({ + where: { tenantId, occurredAt: { gte: periodStart, lt: periodEnd }, ratedUsageItems: { none: {} } }, + }), + tx.aIUsageRecord.findMany({ + where: { tenantId, occurredAt: { gte: periodStart, lt: periodEnd }, ratedUsageItems: { none: {} } }, + }), + ]); + + const ratedItemsData: Prisma.RatedUsageItemCreateManyInput[] = []; + + for (const ev of usageEvents) { + if (ev.meter === "CALL_SECONDS") { + if (!callMinuteItem) continue; // sem preco/minuto configurado — nao cobra, nao inventa preco + const rated = rateCallFlatFallback(ev.quantity, callMinuteItem); + ratedItemsData.push({ + tenantId, + usageEventId: ev.id, + callId: ev.callId, + priceBookItemId: callMinuteItem.id, + quantity: rated.ratedMinutes, + unitPrice: rated.destinationRate, + amount: rated.ratedAmount, + currency: priceBook.currency, + billingPeriodId: period.id, + }); + continue; + } + + const priceType = ACTIVE_DAY_METER_TO_PRICE_TYPE[ev.meter]; + if (!priceType) continue; // RECORDING_BYTES e os meters de IA nao viram UsageEvent (ver cdr.ts / process-*.ts) + const item = resolvePriceBookItem(priceItems, priceType, periodEnd); + if (!item) continue; + const amount = rateActiveDaysProrated(ev.quantity, item.unitPrice, daysInPeriod); + ratedItemsData.push({ + tenantId, + usageEventId: ev.id, + priceBookItemId: item.id, + quantity: ev.quantity, + unitPrice: daysInPeriod > 0 ? item.unitPrice / daysInPeriod : 0, + amount, + currency: priceBook.currency, + billingPeriodId: period.id, + }); + } + + for (const rec of aiUsageRecords) { + const priceType = AI_USAGE_TYPE_TO_PRICE_TYPE[rec.type]; + const item = resolvePriceBookItem(priceItems, priceType, periodEnd); + if (!item) continue; + const amount = + rec.type === "AI_TRANSCRIPTION_SECONDS" + ? rateTranscriptionSeconds(rec.quantity, item.unitPrice) + : rateGenericUsage(rec.quantity, item.unitPrice); + ratedItemsData.push({ + tenantId, + aiUsageRecordId: rec.id, + priceBookItemId: item.id, + quantity: rec.quantity, + unitPrice: item.unitPrice, + amount, + currency: priceBook.currency, + billingPeriodId: period.id, + }); + } + + // RECORDING_BYTES (secao 131): sem ledger de eventos próprio (ver + // comentário em rating-engine.ts) — usa os bytes armazenados AGORA como + // proxy do consumo do período inteiro, decisão documentada em + // docs/BILLING.md. + const storageItem = resolvePriceBookItem(priceItems, "RECORDING_GB_MONTH", periodEnd); + if (storageItem) { + const recordingAgg = await tx.recording.aggregate({ + where: { tenantId, status: "AVAILABLE" }, + _sum: { sizeBytes: true }, + }); + const bytes = Number(recordingAgg._sum.sizeBytes ?? 0n); + if (bytes > 0) { + const amount = rateRecordingBytes(bytes, storageItem.unitPrice); + ratedItemsData.push({ + tenantId, + priceBookItemId: storageItem.id, + quantity: bytes / 1_000_000_000, + unitPrice: storageItem.unitPrice, + amount, + currency: priceBook.currency, + billingPeriodId: period.id, + }); + } + } + + if (ratedItemsData.length > 0) { + await tx.ratedUsageItem.createMany({ data: ratedItemsData }); + } + + const ratedItems = await tx.ratedUsageItem.findMany({ + where: { billingPeriodId: period.id }, + include: { priceBookItem: true }, + }); + + const categoryTotals = new Map(); + for (const item of ratedItems) { + const category = item.priceBookItem ? PRICE_TYPE_TO_CATEGORY[item.priceBookItem.type] : "ADJUSTMENT"; + categoryTotals.set(category, (categoryTotals.get(category) ?? 0) + item.amount); + } + + const subscription = await tx.tenantSubscription.findFirst({ + where: { tenantId, status: { in: ["ACTIVE", "TRIALING"] } }, + include: { planVersion: true }, + orderBy: { startedAt: "desc" }, + }); + if (subscription && subscription.planVersion.basePrice > 0) { + categoryTotals.set("PLAN_BASE", (categoryTotals.get("PLAN_BASE") ?? 0) + subscription.planVersion.basePrice); + } + + const subtotal = [...categoryTotals.values()].reduce((sum, v) => sum + v, 0); + const currency = subscription?.currency ?? priceBook.currency; + + const statement = await tx.billingStatement.create({ + data: { + tenantId, + billingPeriodId: period.id, + currency, + subtotal, + adjustments: 0, + total: subtotal, + items: { + create: [...categoryTotals.entries()].map(([category, amount]) => ({ + tenantId, + category: category as never, + description: CATEGORY_LABEL[category] ?? category, + amount, + })), + }, + }, + }); + + await tx.billingPeriod.update({ + where: { id: period.id }, + data: { status: "CLOSED", closedAt: new Date() }, + }); + + return { periodId: period.id, statementId: statement.id, total: statement.total, ratedCount: ratedItems.length }; + }); + + await recordAuditEvent(prisma, { + action: "BILLING_PERIOD_CLOSE", + tenantId, + userId, + entityType: "billing_period", + entityId: result.periodId, + after: { total: result.total, ratedItemCount: result.ratedCount }, + }); + + return result; +} + +/** + * "Reabrir" um período fechado pra corrigir e recalcular (secao 137): nunca + * volta direto pra OPEN — vira REOPENED, com audit trail (user+motivo), + * deixando visível no histórico que esse período já foi fechado antes. + * `closeBillingPeriod` aceita rodar de novo em cima de um período + * REOPENED (só bloqueia CLOSED/CALCULATING). + * + * **Bug real, achado no teste desta fase**: a primeira versão lia o + * período com `prisma.billingPeriod.findUniqueOrThrow({ where: { id } })` + * SEM tenant context pra descobrir o `tenantId` — mas `billing_periods` tem + * FORCE ROW LEVEL SECURITY (agente.md secao 30), então a leitura sem + * `app.current_tenant_id` não vê a linha, e o "not found" virava 500 (P2025 + * não mapeado, nunca um 404 de verdade). `tenantId` precisa vir explícito + * no request (mesma exceção já aplicada em `closeBillingPeriod`), nunca + * descoberto lendo a própria tabela protegida por RLS. + */ +export async function reopenBillingPeriod(opts: { + periodId: string; + tenantId: string; + userId: string; + reason: string; +}): Promise { + const prisma = getPrismaClient(); + + const period = await withTenantContext(prisma, opts.tenantId, (tx) => + tx.billingPeriod.findUnique({ where: { id: opts.periodId } }), + ); + if (!period || period.tenantId !== opts.tenantId) { + throw new NotFoundException("Periodo de billing nao encontrado para este tenant"); + } + if (period.status !== "CLOSED") { + throw new ConflictException("So' e' possivel reabrir um periodo CLOSED"); + } + + await withTenantContext(prisma, opts.tenantId, (tx) => + tx.billingPeriod.update({ + where: { id: period.id }, + data: { status: "REOPENED", reopenedAt: new Date() }, + }), + ); + + await recordAuditEvent(prisma, { + action: "BILLING_PERIOD_REOPEN", + tenantId: period.tenantId, + userId: opts.userId, + entityType: "billing_period", + entityId: period.id, + after: { reason: opts.reason }, + }); +} diff --git a/apps/api/src/billing/billing-periods.controller.ts b/apps/api/src/billing/billing-periods.controller.ts new file mode 100644 index 0000000..34c212b --- /dev/null +++ b/apps/api/src/billing/billing-periods.controller.ts @@ -0,0 +1,57 @@ +import { Body, Controller, ForbiddenException, Get, Param, Post, 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"; +import { ClosePeriodDto } from "./dto/close-period.dto"; +import { ReopenPeriodDto } from "./dto/reopen-period.dto"; +import { closeBillingPeriod, reopenBillingPeriod } from "./billing-engine.service"; + +/** + * "Fechamentos" (PRODUCT.md, menu Platform > Billing). Fechar/reabrir um + * período é uma ação de platform admin sobre um tenant arbitrário — por + * isso `tenantId` vem no body em vez de vir só do JWT (mesma exceção já + * aplicada a GLOBAL em AIProvider/AIPromptTemplate: `isPlatformUser` + * checado explicitamente na camada de serviço, nunca confiado só na + * permission). Ver histórico do fechamento em `GET /billing/periods` (o + * próprio tenant, escopo do seu JWT). + */ +@UseGuards(JwtAuthGuard, PermissionGuard) +@Controller("billing/periods") +export class BillingPeriodsController { + @RequirePermission("billing.manage") + @Post("close") + async close(@CurrentUser() user: AccessTokenClaims, @Body() dto: ClosePeriodDto) { + if (!(await isPlatformUser(user.sub))) { + throw new ForbiddenException("So' um usuario com role de plataforma pode fechar um periodo de billing"); + } + return closeBillingPeriod({ + tenantId: dto.tenantId, + periodStart: new Date(dto.periodStart), + periodEnd: new Date(dto.periodEnd), + userId: user.sub, + }); + } + + @RequirePermission("billing.manage") + @Post(":id/reopen") + async reopen(@CurrentUser() user: AccessTokenClaims, @Param("id") id: string, @Body() dto: ReopenPeriodDto) { + if (!(await isPlatformUser(user.sub))) { + throw new ForbiddenException("So' um usuario com role de plataforma pode reabrir um periodo de billing"); + } + await reopenBillingPeriod({ periodId: id, tenantId: dto.tenantId, userId: user.sub, reason: dto.reason }); + return { reopened: true }; + } + + @RequirePermission("billing.view") + @Get() + async list(@CurrentUser() user: AccessTokenClaims) { + const tenantId = user.tenantId!; + const prisma = getPrismaClient(); + return withTenantContext(prisma, tenantId, (tx) => + tx.billingPeriod.findMany({ where: { tenantId }, orderBy: { periodStart: "desc" } }), + ); + } +} diff --git a/apps/api/src/billing/billing-statements.controller.ts b/apps/api/src/billing/billing-statements.controller.ts new file mode 100644 index 0000000..da2a683 --- /dev/null +++ b/apps/api/src/billing/billing-statements.controller.ts @@ -0,0 +1,46 @@ +import { Controller, Get, NotFoundException, Param, UseGuards } from "@nestjs/common"; +import { getPrismaClient, withTenantContext } from "@b2bcall/database"; +import 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 Statement" / "Relatorio de Consumo" (agente.md secao 138-139) — + * nunca chamado de "invoice"/"nota fiscal" na UI (PRODUCT.md, Operating + * Context). Sempre escopado ao próprio tenant do JWT (secao 31) — nunca um + * id de statement de outro tenant, RLS + WHERE tenantId garantem os dois. + */ +@UseGuards(JwtAuthGuard, PermissionGuard) +@Controller("billing/statements") +export class BillingStatementsController { + @RequirePermission("billing.view") + @Get() + async list(@CurrentUser() user: AccessTokenClaims) { + const tenantId = user.tenantId!; + const prisma = getPrismaClient(); + return withTenantContext(prisma, tenantId, (tx) => + tx.billingStatement.findMany({ + where: { tenantId }, + include: { billingPeriod: true }, + orderBy: { generatedAt: "desc" }, + }), + ); + } + + @RequirePermission("billing.view") + @Get(":id") + async get(@CurrentUser() user: AccessTokenClaims, @Param("id") id: string) { + const tenantId = user.tenantId!; + const prisma = getPrismaClient(); + const statement = await withTenantContext(prisma, tenantId, (tx) => + tx.billingStatement.findFirst({ + where: { id, tenantId }, + include: { billingPeriod: true, items: true }, + }), + ); + if (!statement) throw new NotFoundException(); + return statement; + } +} diff --git a/apps/api/src/billing/billing.module.ts b/apps/api/src/billing/billing.module.ts new file mode 100644 index 0000000..9fb7469 --- /dev/null +++ b/apps/api/src/billing/billing.module.ts @@ -0,0 +1,19 @@ +import { Module } from "@nestjs/common"; +import { PriceBooksController } from "./price-books.controller"; +import { RateDecksController } from "./rate-decks.controller"; +import { PlanVersionsController } from "./plan-versions.controller"; +import { SubscriptionsController } from "./subscriptions.controller"; +import { BillingPeriodsController } from "./billing-periods.controller"; +import { BillingStatementsController } from "./billing-statements.controller"; + +@Module({ + controllers: [ + PriceBooksController, + RateDecksController, + PlanVersionsController, + SubscriptionsController, + BillingPeriodsController, + BillingStatementsController, + ], +}) +export class BillingModule {} diff --git a/apps/api/src/billing/dto/close-period.dto.ts b/apps/api/src/billing/dto/close-period.dto.ts new file mode 100644 index 0000000..d220a4b --- /dev/null +++ b/apps/api/src/billing/dto/close-period.dto.ts @@ -0,0 +1,12 @@ +import { IsDateString, IsUUID } from "class-validator"; + +export class ClosePeriodDto { + @IsUUID() + tenantId!: string; + + @IsDateString() + periodStart!: string; + + @IsDateString() + periodEnd!: string; +} diff --git a/apps/api/src/billing/dto/create-plan-version.dto.ts b/apps/api/src/billing/dto/create-plan-version.dto.ts new file mode 100644 index 0000000..d3627cc --- /dev/null +++ b/apps/api/src/billing/dto/create-plan-version.dto.ts @@ -0,0 +1,22 @@ +import { IsDateString, IsNumber, IsOptional, IsString, IsUUID, MaxLength, Min } from "class-validator"; + +export class CreatePlanVersionDto { + @IsUUID() + planId!: string; + + @IsNumber() + @Min(0) + basePrice!: number; + + @IsOptional() + @IsString() + @MaxLength(3) + currency?: string; + + @IsDateString() + effectiveFrom!: string; + + @IsOptional() + @IsDateString() + effectiveUntil?: string; +} diff --git a/apps/api/src/billing/dto/create-price-book.dto.ts b/apps/api/src/billing/dto/create-price-book.dto.ts new file mode 100644 index 0000000..f6237d7 --- /dev/null +++ b/apps/api/src/billing/dto/create-price-book.dto.ts @@ -0,0 +1,55 @@ +import { IsArray, IsBoolean, IsDateString, IsIn, IsNumber, IsOptional, IsString, MaxLength, ValidateNested } from "class-validator"; +import { Type } from "class-transformer"; + +const PRICE_ITEM_TYPES = [ + "BASE_SUBSCRIPTION", + "EXTENSION_MONTH", + "AGENT_MONTH", + "TRUNK_MONTH", + "CALL", + "CALL_MINUTE", + "FIXED_MINUTE", + "MOBILE_MINUTE", + "INTERNATIONAL_MINUTE", + "AI_TRANSCRIPTION_MINUTE", + "AI_ANALYSIS_CALL", + "AI_INPUT_TOKEN", + "AI_OUTPUT_TOKEN", + "RECORDING_GB_MONTH", +] as const; + +export class CreatePriceBookItemDto { + @IsIn(PRICE_ITEM_TYPES) + type!: (typeof PRICE_ITEM_TYPES)[number]; + + @IsNumber() + unitPrice!: number; + + @IsDateString() + effectiveFrom!: string; + + @IsOptional() + @IsDateString() + effectiveUntil?: string; +} + +export class CreatePriceBookDto { + @IsString() + @MaxLength(120) + name!: string; + + @IsOptional() + @IsString() + @MaxLength(3) + currency?: string; + + @IsOptional() + @IsBoolean() + isDefault?: boolean; + + @IsOptional() + @IsArray() + @ValidateNested({ each: true }) + @Type(() => CreatePriceBookItemDto) + items?: CreatePriceBookItemDto[]; +} diff --git a/apps/api/src/billing/dto/create-rate-deck.dto.ts b/apps/api/src/billing/dto/create-rate-deck.dto.ts new file mode 100644 index 0000000..8760475 --- /dev/null +++ b/apps/api/src/billing/dto/create-rate-deck.dto.ts @@ -0,0 +1,60 @@ +import { IsArray, IsBoolean, IsDateString, IsIn, IsInt, IsNumber, IsOptional, IsString, Max, MaxLength, Min, ValidateNested } from "class-validator"; +import { Type } from "class-transformer"; + +const DESTINATION_TYPES = ["FIXED", "MOBILE", "INTERNATIONAL"] as const; + +export class CreateRateDeckEntryDto { + @IsString() + @MaxLength(20) + prefix!: string; + + @IsString() + @MaxLength(80) + destinationName!: string; + + @IsIn(DESTINATION_TYPES) + destinationType!: (typeof DESTINATION_TYPES)[number]; + + @IsNumber() + pricePerMinute!: number; + + @IsOptional() + @IsInt() + @Min(1) + @Max(3600) + billingIncrementSeconds?: number; + + @IsOptional() + @IsInt() + @Min(0) + @Max(3600) + minimumSeconds?: number; + + @IsOptional() + @IsNumber() + @Min(0) + connectionFee?: number; + + @IsDateString() + validFrom!: string; + + @IsOptional() + @IsDateString() + validUntil?: string; +} + +export class CreateRateDeckDto { + @IsString() + @MaxLength(120) + name!: string; + + @IsOptional() + @IsBoolean() + isDefault?: boolean; + + @IsOptional() + @IsArray() + @ValidateNested({ each: true }) + @Type(() => CreateRateDeckEntryDto) + entries?: CreateRateDeckEntryDto[]; +} diff --git a/apps/api/src/billing/dto/create-subscription.dto.ts b/apps/api/src/billing/dto/create-subscription.dto.ts new file mode 100644 index 0000000..ba593b5 --- /dev/null +++ b/apps/api/src/billing/dto/create-subscription.dto.ts @@ -0,0 +1,23 @@ +import { IsDateString, IsInt, IsOptional, IsString, IsUUID, Max, MaxLength, Min } from "class-validator"; + +export class CreateSubscriptionDto { + @IsUUID() + tenantId!: string; + + @IsUUID() + planVersionId!: string; + + @IsOptional() + @IsDateString() + startedAt?: string; + + @IsInt() + @Min(1) + @Max(28) + billingCycleAnchor!: number; + + @IsOptional() + @IsString() + @MaxLength(3) + currency?: string; +} diff --git a/apps/api/src/billing/dto/reopen-period.dto.ts b/apps/api/src/billing/dto/reopen-period.dto.ts new file mode 100644 index 0000000..73ee6e4 --- /dev/null +++ b/apps/api/src/billing/dto/reopen-period.dto.ts @@ -0,0 +1,11 @@ +import { IsString, IsUUID, MaxLength, MinLength } from "class-validator"; + +export class ReopenPeriodDto { + @IsUUID() + tenantId!: string; + + @IsString() + @MinLength(3) + @MaxLength(500) + reason!: string; +} diff --git a/apps/api/src/billing/plan-versions.controller.ts b/apps/api/src/billing/plan-versions.controller.ts new file mode 100644 index 0000000..d0f7e19 --- /dev/null +++ b/apps/api/src/billing/plan-versions.controller.ts @@ -0,0 +1,66 @@ +import { Body, Controller, ForbiddenException, Get, Param, Post, UseGuards } from "@nestjs/common"; +import { getPrismaClient } from "@b2bcall/database"; +import { recordAuditEvent, 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"; +import { CreatePlanVersionDto } from "./dto/create-plan-version.dto"; + +/** + * "plan_versions" (agente.md secao 126: "Precos e limites devem ser + * versionados"). Versiona só o preço base — ver comentário no schema. + * `version` é sempre a proxima sequencial do plano (nunca escolhida pelo + * client, agente.md secao 233: nunca confiar em input do client pra + * invariante do sistema). + */ +@UseGuards(JwtAuthGuard, PermissionGuard) +@Controller("billing/plan-versions") +export class PlanVersionsController { + @RequirePermission("pricing.manage") + @Post() + async create(@CurrentUser() user: AccessTokenClaims, @Body() dto: CreatePlanVersionDto) { + if (!(await isPlatformUser(user.sub))) { + throw new ForbiddenException("So' um usuario com role de plataforma pode versionar planos"); + } + const prisma = getPrismaClient(); + + const plan = await prisma.plan.findUnique({ where: { id: dto.planId } }); + if (!plan) throw new ForbiddenException("Plano nao encontrado"); + + const lastVersion = await prisma.planVersion.findFirst({ + where: { planId: dto.planId }, + orderBy: { version: "desc" }, + }); + const nextVersion = (lastVersion?.version ?? 0) + 1; + + const planVersion = await prisma.planVersion.create({ + data: { + planId: dto.planId, + version: nextVersion, + basePrice: dto.basePrice, + currency: dto.currency ?? "BRL", + effectiveFrom: new Date(dto.effectiveFrom), + effectiveUntil: dto.effectiveUntil ? new Date(dto.effectiveUntil) : null, + }, + }); + + await recordAuditEvent(prisma, { + action: "PLAN_VERSION_CREATE", + tenantId: null, + userId: user.sub, + entityType: "plan_version", + entityId: planVersion.id, + after: { planId: plan.id, version: planVersion.version, basePrice: planVersion.basePrice }, + }); + + return planVersion; + } + + @RequirePermission("pricing.manage") + @Get("by-plan/:planId") + async listByPlan(@Param("planId") planId: string) { + const prisma = getPrismaClient(); + return prisma.planVersion.findMany({ where: { planId }, orderBy: { version: "desc" } }); + } +} diff --git a/apps/api/src/billing/price-books.controller.ts b/apps/api/src/billing/price-books.controller.ts new file mode 100644 index 0000000..e064e7c --- /dev/null +++ b/apps/api/src/billing/price-books.controller.ts @@ -0,0 +1,78 @@ +import { Body, Controller, ForbiddenException, Get, NotFoundException, Param, Post, UseGuards } from "@nestjs/common"; +import { getPrismaClient } from "@b2bcall/database"; +import { recordAuditEvent, 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"; +import { CreatePriceBookDto } from "./dto/create-price-book.dto"; + +async function assertPlatformUser(userId: string): Promise { + if (!(await isPlatformUser(userId))) { + throw new ForbiddenException("So' um usuario com role de plataforma pode gerenciar price books"); + } +} + +/** + * "price_books"/"price_book_items" (agente.md secao 128) — catálogo + * global da plataforma, sem tenant_id (mesmo padrão de `Plan`), gerenciado + * só por platform admin. Um `Tenant` escolhe qual usar via + * `Tenant.priceBookId` (null = o que tiver `isDefault=true`); a atribuição + * em si é uma ação de `tenants.manage`, fora do escopo deste controller. + */ +@UseGuards(JwtAuthGuard, PermissionGuard) +@Controller("billing/price-books") +export class PriceBooksController { + @RequirePermission("pricing.manage") + @Post() + async create(@CurrentUser() user: AccessTokenClaims, @Body() dto: CreatePriceBookDto) { + await assertPlatformUser(user.sub); + const prisma = getPrismaClient(); + + const priceBook = await prisma.priceBook.create({ + data: { + name: dto.name, + currency: dto.currency ?? "BRL", + isDefault: dto.isDefault ?? false, + items: dto.items + ? { + create: dto.items.map((item) => ({ + type: item.type, + unitPrice: item.unitPrice, + effectiveFrom: new Date(item.effectiveFrom), + effectiveUntil: item.effectiveUntil ? new Date(item.effectiveUntil) : null, + })), + } + : undefined, + }, + include: { items: true }, + }); + + await recordAuditEvent(prisma, { + action: "PRICE_BOOK_CREATE", + tenantId: null, + userId: user.sub, + entityType: "price_book", + entityId: priceBook.id, + after: { name: priceBook.name, isDefault: priceBook.isDefault }, + }); + + return priceBook; + } + + @RequirePermission("pricing.manage") + @Get() + async list() { + const prisma = getPrismaClient(); + return prisma.priceBook.findMany({ include: { items: true }, orderBy: { name: "asc" } }); + } + + @RequirePermission("pricing.manage") + @Get(":id") + async get(@Param("id") id: string) { + const prisma = getPrismaClient(); + const priceBook = await prisma.priceBook.findUnique({ where: { id }, include: { items: true } }); + if (!priceBook) throw new NotFoundException(); + return priceBook; + } +} diff --git a/apps/api/src/billing/rate-decks.controller.ts b/apps/api/src/billing/rate-decks.controller.ts new file mode 100644 index 0000000..22acf4d --- /dev/null +++ b/apps/api/src/billing/rate-decks.controller.ts @@ -0,0 +1,84 @@ +import { Body, Controller, ForbiddenException, Get, NotFoundException, Param, Post, UseGuards } from "@nestjs/common"; +import { getPrismaClient } from "@b2bcall/database"; +import { recordAuditEvent, 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"; +import { CreateRateDeckDto } from "./dto/create-rate-deck.dto"; + +async function assertPlatformUser(userId: string): Promise { + if (!(await isPlatformUser(userId))) { + throw new ForbiddenException("So' um usuario com role de plataforma pode gerenciar rate decks"); + } +} + +/** + * "rate_decks"/"rate_deck_entries" (agente.md secao 129) — catálogo global + * de tarifas por prefixo de destino, mesmo padrão de escopo de + * `PriceBooksController`. `RatingEngine.longestPrefixMatch` + * (packages/billing) consome `entries` pra tarifar `CALL_SECONDS` por + * destino — hoje o fallback plano é sempre usado (ver + * `BillingEngineService`, `Call.calledNumber` ainda não é populado pelo + * CDR), mas o cadastro fica disponível pra quando essa lacuna for fechada. + */ +@UseGuards(JwtAuthGuard, PermissionGuard) +@Controller("billing/rate-decks") +export class RateDecksController { + @RequirePermission("pricing.manage") + @Post() + async create(@CurrentUser() user: AccessTokenClaims, @Body() dto: CreateRateDeckDto) { + await assertPlatformUser(user.sub); + const prisma = getPrismaClient(); + + const rateDeck = await prisma.rateDeck.create({ + data: { + name: dto.name, + isDefault: dto.isDefault ?? false, + entries: dto.entries + ? { + create: dto.entries.map((entry) => ({ + prefix: entry.prefix, + destinationName: entry.destinationName, + destinationType: entry.destinationType, + pricePerMinute: entry.pricePerMinute, + billingIncrementSeconds: entry.billingIncrementSeconds ?? 60, + minimumSeconds: entry.minimumSeconds ?? 0, + connectionFee: entry.connectionFee ?? 0, + validFrom: new Date(entry.validFrom), + validUntil: entry.validUntil ? new Date(entry.validUntil) : null, + })), + } + : undefined, + }, + include: { entries: true }, + }); + + await recordAuditEvent(prisma, { + action: "RATE_DECK_CREATE", + tenantId: null, + userId: user.sub, + entityType: "rate_deck", + entityId: rateDeck.id, + after: { name: rateDeck.name, isDefault: rateDeck.isDefault }, + }); + + return rateDeck; + } + + @RequirePermission("pricing.manage") + @Get() + async list() { + const prisma = getPrismaClient(); + return prisma.rateDeck.findMany({ include: { entries: true }, orderBy: { name: "asc" } }); + } + + @RequirePermission("pricing.manage") + @Get(":id") + async get(@Param("id") id: string) { + const prisma = getPrismaClient(); + const rateDeck = await prisma.rateDeck.findUnique({ where: { id }, include: { entries: true } }); + if (!rateDeck) throw new NotFoundException(); + return rateDeck; + } +} diff --git a/apps/api/src/billing/subscriptions.controller.ts b/apps/api/src/billing/subscriptions.controller.ts new file mode 100644 index 0000000..691e910 --- /dev/null +++ b/apps/api/src/billing/subscriptions.controller.ts @@ -0,0 +1,75 @@ +import { Body, Controller, ForbiddenException, Get, Param, Post, UseGuards } from "@nestjs/common"; +import { getPrismaClient, withTenantContext } from "@b2bcall/database"; +import { recordAuditEvent, 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"; +import { CreateSubscriptionDto } from "./dto/create-subscription.dto"; + +/** + * "tenant_subscriptions" (agente.md secao 127) — qual `PlanVersion` (preço + * vigente) um tenant assinou e em que dia do mês fecha o período de + * billing dele (`billingCycleAnchor`). Ação de platform admin (o tenant + * não escolhe o próprio preço); um tenant pode ter várias linhas ao longo + * do tempo (histórico de mudança de plano/preço), nunca UPDATE no preço de + * uma assinatura já ativa — sempre uma nova linha. + */ +@UseGuards(JwtAuthGuard, PermissionGuard) +@Controller("billing/subscriptions") +export class SubscriptionsController { + @RequirePermission("billing.manage") + @Post() + async create(@CurrentUser() user: AccessTokenClaims, @Body() dto: CreateSubscriptionDto) { + if (!(await isPlatformUser(user.sub))) { + throw new ForbiddenException("So' um usuario com role de plataforma pode gerenciar assinaturas"); + } + const prisma = getPrismaClient(); + + const planVersion = await prisma.planVersion.findUnique({ where: { id: dto.planVersionId } }); + if (!planVersion) throw new ForbiddenException("PlanVersion nao encontrada"); + + // `tenant_subscriptions` tem RLS (tenant-scoped) mesmo essa sendo uma + // ação de platform admin sobre um tenant arbitrário — precisa do + // contexto igual a qualquer outra escrita tenant-scoped (secao 30). + const subscription = await withTenantContext(prisma, dto.tenantId, (tx) => + tx.tenantSubscription.create({ + data: { + tenantId: dto.tenantId, + planVersionId: dto.planVersionId, + status: "ACTIVE", + startedAt: dto.startedAt ? new Date(dto.startedAt) : new Date(), + billingCycleAnchor: dto.billingCycleAnchor, + currency: dto.currency ?? planVersion.currency, + }, + }), + ); + + await recordAuditEvent(prisma, { + action: "TENANT_SUBSCRIPTION_CREATE", + tenantId: dto.tenantId, + userId: user.sub, + entityType: "tenant_subscription", + entityId: subscription.id, + after: { planVersionId: subscription.planVersionId, billingCycleAnchor: subscription.billingCycleAnchor }, + }); + + return subscription; + } + + @RequirePermission("billing.manage") + @Get("by-tenant/:tenantId") + async listByTenant(@CurrentUser() user: AccessTokenClaims, @Param("tenantId") tenantId: string) { + if (!(await isPlatformUser(user.sub)) && user.tenantId !== tenantId) { + throw new ForbiddenException("Sem acesso as assinaturas deste tenant"); + } + const prisma = getPrismaClient(); + return withTenantContext(prisma, tenantId, (tx) => + tx.tenantSubscription.findMany({ + where: { tenantId }, + include: { planVersion: true }, + orderBy: { startedAt: "desc" }, + }), + ); + } +} diff --git a/apps/api/src/common/guards/permission.guard.ts b/apps/api/src/common/guards/permission.guard.ts index fe75e5b..0a5f021 100644 --- a/apps/api/src/common/guards/permission.guard.ts +++ b/apps/api/src/common/guards/permission.guard.ts @@ -5,10 +5,21 @@ import { PERMISSION_KEY } from "../decorators/require-permission.decorator"; import type { AuthenticatedRequest } from "./jwt-auth.guard"; /** - * Roda depois do JwtAuthGuard. Exige que a rota tenha um tenant selecionado - * (agente.md secao 31: nunca confiar em tenant_id do frontend — aqui vem só - * do JWT, nunca do body/query) e que o usuário tenha a permission marcada - * via @RequirePermission() (secao 145/146). + * Roda depois do JwtAuthGuard. Exige que o usuário tenha a permission + * marcada via @RequirePermission() (secao 145/146), resolvida sempre a + * partir do JWT (secao 31: nunca confiar em tenant_id do frontend). + * + * Um usuário PLATFORM puro (sem NENHUMA TenantMembership — ex.: o platform + * super admin recém-criado) nunca tem um tenant pra selecionar em + * `/auth/select-tenant` (`/auth/tenants` retorna vazio pra ele), então + * `user.tenantId` legitimamente nunca vai existir no token dele. Bug real + * encontrado testando `PlatformOverviewController` (agente.md secao 163): + * antes desta correção, exigir `tenantId` incondicionalmente deixava + * QUALQUER endpoint com @RequirePermission inacessível pra esse usuário, + * mesmo os platform-only. Corrigido: sem tenantId, ainda tenta a permission + * em escopo PLATFORM (`userHasPermission` com tenantId undefined já filtra + * só roles com tenantId null); só barra de fato quem não tem a permission + * em nenhum escopo. */ @Injectable() export class PermissionGuard implements CanActivate { @@ -29,13 +40,14 @@ export class PermissionGuard implements CanActivate { if (!user) { throw new ForbiddenException("Nao autenticado"); } - if (!user.tenantId) { - throw new ForbiddenException("Nenhum tenant selecionado (use /auth/select-tenant)"); - } - const allowed = await userHasPermission(user.sub, permissionKey, user.tenantId); + const allowed = await userHasPermission(user.sub, permissionKey, user.tenantId ?? undefined); if (!allowed) { - throw new ForbiddenException(`Permissao necessaria: ${permissionKey}`); + throw new ForbiddenException( + user.tenantId + ? `Permissao necessaria: ${permissionKey}` + : `Permissao necessaria: ${permissionKey} (nenhum tenant selecionado, checado so' em escopo PLATFORM — use /auth/select-tenant se a permissao for de tenant)`, + ); } return true; diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index 6cc0751..486b248 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -9,8 +9,10 @@ import rateLimit from "@fastify/rate-limit"; import { AppModule } from "./app.module"; import { DomainExceptionFilter } from "./common/filters/domain-exception.filter"; import { runRetentionSweep } from "./recordings/retention-sweep"; +import { runActiveDaySweep } from "./billing/active-day-sweep"; const RETENTION_SWEEP_INTERVAL_MS = 60 * 60 * 1000; +const ACTIVE_DAY_SWEEP_INTERVAL_MS = 60 * 60 * 1000; async function bootstrap() { const app = await NestFactory.create( @@ -63,6 +65,13 @@ async function bootstrap() { setInterval(() => { runRetentionSweep().catch((err) => console.error("falha na varredura de retencao", err)); }, RETENTION_SWEEP_INTERVAL_MS); + + // Usage metering diario (agente.md secao 131: EXTENSION/AGENT/TRUNK + // ACTIVE_DAY) — mesmo padrao da varredura de retencao acima. + runActiveDaySweep().catch((err) => console.error("falha na varredura de uso diario (boot)", err)); + setInterval(() => { + runActiveDaySweep().catch((err) => console.error("falha na varredura de uso diario", err)); + }, ACTIVE_DAY_SWEEP_INTERVAL_MS); } bootstrap(); diff --git a/apps/api/src/platform/platform-overview.controller.ts b/apps/api/src/platform/platform-overview.controller.ts new file mode 100644 index 0000000..c9fd4ed --- /dev/null +++ b/apps/api/src/platform/platform-overview.controller.ts @@ -0,0 +1,85 @@ +import { Controller, ForbiddenException, Get, UseGuards } from "@nestjs/common"; +import { getPrismaClient } 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"; + +/** + * "Dashboard Platform" (agente.md secao 163) — só platform admin (mesmo + * padrão de isPlatformUser já usado pra escrita GLOBAL em AIProvider/ + * AIPromptTemplate). Consulta direto (sem withTenantContext — precisa + * agregar TODOS os tenants, não faz sentido sob RLS de um tenant só). + */ +@UseGuards(JwtAuthGuard, PermissionGuard) +@Controller("platform") +export class PlatformOverviewController { + @RequirePermission("tenants.view") + @Get("overview") + async overview(@CurrentUser() user: AccessTokenClaims) { + const isPlatform = await isPlatformUser(user.sub); + if (!isPlatform) { + throw new ForbiddenException("So' um usuario com role de plataforma pode ver o dashboard da plataforma"); + } + + const prisma = getPrismaClient(); + const todayStart = new Date(); + todayStart.setUTCHours(0, 0, 0, 0); + const monthStart = new Date(Date.UTC(new Date().getUTCFullYear(), new Date().getUTCMonth(), 1)); + + const [ + tenantsActive, + tenantsTotal, + extensionsTotal, + agentsTotal, + callsCurrent, + callsToday, + cpsCapacity, + aiUsageThisMonth, + recordingBytesAgg, + ] = await Promise.all([ + prisma.tenant.count({ where: { status: "ACTIVE", deletedAt: null } }), + prisma.tenant.count({ where: { deletedAt: null } }), + prisma.extension.count({ where: { deletedAt: null } }), + prisma.agent.count({ where: { deletedAt: null } }), + prisma.call.count({ where: { endAt: null } }), + prisma.call.count({ where: { createdAt: { gte: todayStart } } }), + prisma.plan.aggregate({ _sum: { maxCps: true } }), + prisma.aIUsageRecord.groupBy({ + by: ["type"], + where: { occurredAt: { gte: monthStart } }, + _sum: { quantity: true }, + }), + prisma.recording.aggregate({ _sum: { sizeBytes: true } }), + ]); + + return { + tenantsActive, + tenantsTotal, + extensionsTotal, + agentsTotal, + callsCurrent, + callsToday, + // Deployment desta lab tem 1 unico container FreeSWITCH — sem tabela + // de nodes ainda pra descobrir isso dinamicamente (nao existe + // clustering multi-node nesta fase). + freeswitchNodes: 1, + // Soma dos tetos de CPS configurados por plano em todos os tenants — + // capacidade OUTORGADA, nao consumo em tempo real (isso vive no + // token bucket do Redis do predictive-dialer, apps/api nao le de + // la ainda). + cpsCapacityConfigured: cpsCapacity._sum.maxCps ?? null, + aiUsageThisMonth: Object.fromEntries( + aiUsageThisMonth.map((row) => [row.type, row._sum.quantity ?? 0]), + ), + recordingStorageBytes: Number(recordingBytesAgg._sum.sizeBytes ?? 0n), + // Precisa de BillingPeriod/BillingStatement fechados de verdade + // (fase Billing, em construcao) — nenhum periodo foi fechado ainda + // nesta lab, entao nao ha numero real pra mostrar. null e' honesto, + // nao 0. + monthlyConsumption: null as number | null, + estimatedRevenue: null as number | null, + }; + } +} diff --git a/apps/api/src/platform/platform.module.ts b/apps/api/src/platform/platform.module.ts new file mode 100644 index 0000000..bd1b709 --- /dev/null +++ b/apps/api/src/platform/platform.module.ts @@ -0,0 +1,7 @@ +import { Module } from "@nestjs/common"; +import { PlatformOverviewController } from "./platform-overview.controller"; + +@Module({ + controllers: [PlatformOverviewController], +}) +export class PlatformModule {} diff --git a/apps/freeswitch-events/src/cdr.ts b/apps/freeswitch-events/src/cdr.ts index f912672..73382f5 100644 --- a/apps/freeswitch-events/src/cdr.ts +++ b/apps/freeswitch-events/src/cdr.ts @@ -92,7 +92,7 @@ export async function persistCallEvent(normalized: NormalizedEvent): Promise { +async function finalizeCall(tx: Prisma.TransactionClient, callId: string, tenantId: string): Promise { const call = await tx.call.findUniqueOrThrow({ where: { id: callId } }); const talkTime = seconds(call.bridgeAt, call.endAt); + const billableSeconds = talkTime ?? 0; await tx.call.update({ where: { id: callId }, @@ -166,7 +167,27 @@ async function finalizeCall(tx: Prisma.TransactionClient, callId: string): Promi waitTime: seconds(call.queueEnterAt, call.agentAnswerAt), talkTime, durationSeconds: seconds(call.createdAt, call.endAt), - billableSeconds: talkTime ?? 0, + billableSeconds, }, }); + + // "Chamada faturável" (agente.md secao 133) gera o UsageEvent que o + // RatingEngine (packages/billing) consome no fechamento do período — + // nunca calcula o valor aqui, só registra o fato bruto (segundos + // faturáveis). Chamada sem talk time (nunca bridgeou) não gera evento — + // nada a cobrar. + if (billableSeconds > 0) { + await tx.usageEvent.create({ + data: { + tenantId, + callId, + meter: "CALL_SECONDS", + quantity: billableSeconds, + unit: "seconds", + sourceType: "call", + sourceId: callId, + occurredAt: call.endAt ?? new Date(), + }, + }); + } } diff --git a/docs/BILLING.md b/docs/BILLING.md new file mode 100644 index 0000000..ecaa77e --- /dev/null +++ b/docs/BILLING.md @@ -0,0 +1,163 @@ +# Billing (agente.md secao 120-139) + +"Criar billing desde o início. Não tratar cobrança como relatório +calculado posteriormente de maneira improvisada" (secao 125). Fecha a +PHASE 22 (Usage Metering + Billing) junto com a IA usage metering que já +vinha desde a PHASE 20/21 (`AIUsageRecord`). + +## Modelo de dados + +Catálogos GLOBAIS da plataforma (sem `tenant_id`, mesmo padrão de `Plan`), +gerenciados só por platform admin: + +- `PriceBook`/`PriceBookItem` (secao 128) — preço unitário por + `PriceItemType`, versionado por `effectiveFrom`/`effectiveUntil`. +- `RateDeck`/`RateDeckEntry` (secao 129) — tarifa por prefixo de destino + (longest-prefix match), `pricePerMinute`/`billingIncrementSeconds`/ + `minimumSeconds`/`connectionFee`. +- `PlanVersion` (secao 126) — só o preço base da assinatura é versionado; + os limites (`max_extensions` etc.) continuam em `Plan` direto, sem + versionamento próprio (mudam raramente nesta fase do produto — + simplificação conhecida). + +`Tenant.priceBookId`/`rateDeckId` (null = usa o que tiver `isDefault=true`, +mesma convenção de `Queue.aiPrivacyLevel`) escolhem qual catálogo se +aplica a cada tenant. + +Tenant-scoped, com RLS: + +- `TenantSubscription` (secao 127) — qual `PlanVersion` o tenant assinou e + `billingCycleAnchor` (dia do mês, 1-28). Histórico: nunca UPDATE no + preço de uma assinatura ativa, sempre uma nova linha. +- `UsageEvent` (secao 131) — ledger imutável de uso bruto (só INSERT, + nunca UPDATE/DELETE, mesma disciplina de `AIUsageRecord` desde a PHASE + 20). Meters: `CALL_SECONDS`, `EXTENSION_ACTIVE_DAY`, `AGENT_ACTIVE_DAY`, + `TRUNK_ACTIVE_DAY`. Os 4 meters de IA do enum (`AI_TRANSCRIPTION_SECONDS` + etc.) existem só pra bater com a especificação — quem escreve esse uso + na prática é `AIUsageRecord` (ledger próprio, criado antes da fase + Billing existir); o `RatingEngine` lê os dois ledgers, nunca duplica. +- `RatedUsageItem` — 1 linha por evento tarifado (`usageEventId` OU + `aiUsageRecordId`, nunca os dois), referenciando o `PriceBookItem`/ + `RateDeckEntry` usado. Granularidade fina de propósito (secao 233: + "immutable usage ledger") — a agregação por categoria só acontece no + `BillingStatementItem`. +- `BillingPeriod` (secao 134, 137) — `OPEN → CALCULATING → CLOSED`. + Fechamento imutável: fechar de novo um `CLOSED` é 409, só + `POST /billing/periods/:id/reopen` (audit trail com motivo) volta pra + `REOPENED`, e só a partir daí um novo `close` roda de novo. +- `BillingStatement`/`BillingStatementItem` (secao 138-139) — o que o + tenant vê (`GET /billing/statements`), agregado por + `BillingStatementCategory`. Nunca chamado de "invoice"/"nota fiscal" na + UI (PRODUCT.md). + +## Orquestração (`apps/api/src/billing/billing-engine.service.ts`) + +`packages/billing` (`RatingEngine`) é matemática pura, sem I/O — recebe +linhas já buscadas do banco (`*Like` interfaces, não os tipos do Prisma) e +devolve valores calculados: `longestPrefixMatch`, `resolvePriceBookItem` +(vigência por `effectiveFrom`/`effectiveUntil`), `rateCallByDestination`, +`rateCallFlatFallback`, `rateGenericUsage`, `rateActiveDaysProrated`, +`rateTranscriptionSeconds`, `rateRecordingBytes`. Tem teste unitário +próprio dessas funções. + +`closeBillingPeriod(tenantId, periodStart, periodEnd, userId)` é quem faz +I/O: resolve o `PriceBook` vigente do tenant (ou o default), busca +`UsageEvent`/`AIUsageRecord` do período ainda sem `RatedUsageItem` +(`ratedUsageItems: { none: {} }`), tarifa cada um com o `RatingEngine`, +grava os `RatedUsageItem`s, soma `PLAN_BASE` da `TenantSubscription` +ativa, agrega por categoria em `BillingStatementItem`, fecha o +`BillingPeriod`. Tudo dentro de um único `withTenantContext` (atômico). + +`RECORDING_BYTES` não tem `UsageEvent` próprio (ver comentário no +schema) — usa `Recording.sizeBytes` somado NO MOMENTO do fechamento como +proxy do consumo do período inteiro (não faz média ponderada por dia +armazenado). Documentado aqui porque é a maior liberdade tomada na +implementação: correto o bastante pra fechar o período, mas superfatura +um tenant que reduziu MUITO o volume de gravações no meio do período e +subfatura o oposto. + +## Escritores do ledger `UsageEvent` + +- `CALL_SECONDS`: `apps/freeswitch-events/src/cdr.ts::finalizeCall`, junto + com o cálculo de `billableSeconds` (mesma transação do CDR) — só grava + se `billableSeconds > 0` (chamada que nunca bridgeou não gera evento). +- `EXTENSION_ACTIVE_DAY`/`AGENT_ACTIVE_DAY`/`TRUNK_ACTIVE_DAY`: + `apps/api/src/billing/active-day-sweep.ts::runActiveDaySweep`, boot + + de hora em hora (mesmo padrão de `runRetentionSweep`). 1 evento por + recurso ativo por dia — idempotente dentro do mesmo dia (checa + existência antes de inserir; sem constraint única no banco pra isso, + limitação conhecida documentada no próprio arquivo). + +## **Lacuna real, conhecida**: `CALL_SECONDS` sempre usa o fallback plano + +`Call.calledNumber` ainda não é populado pelo CDR (PHASE 17, TODO.md) — +não dá pra fazer o longest-prefix match do `RateDeck` (secao 129) por +destino real. Por isso `closeBillingPeriod` sempre chama +`rateCallFlatFallback` (contra `PriceBookItem` tipo `CALL_MINUTE`), +nunca `rateCallByDestination`/`longestPrefixMatch` contra um `RateDeck`. +`RateDeck`/`RateDeckEntry` ficam cadastráveis via API e testados +isoladamente (unit test do `RatingEngine`), mas não exercitados ponta a +ponta em `closeBillingPeriod` até essa lacuna do CDR fechar. + +## Permissions + +`pricing.manage` (PriceBook/RateDeck/PlanVersion) e `billing.manage` +(TenantSubscription, fechar/reabrir período) são ações de platform admin +sobre um tenant arbitrário — `tenantId` vem explícito no body (mesma +exceção já usada em GLOBAL de `AIProvider`/`AIPromptTemplate`), e +`isPlatformUser` é checado explicitamente na camada de serviço, nunca só +confiado na permission (o seed de RBAC dá `billing.manage`/`billing.view` +também pro `tenant_admin`, mas essas rotas continuam platform-only via +`isPlatformUser` — revisar se um dia existir uma ação de billing que o +próprio tenant deva poder fazer). `billing.view` é do tenant, sempre +escopado ao próprio JWT. + +## O que foi testado de verdade + +Ponta a ponta contra a API real (`apps/api` no host) e Postgres real com +RLS: tenant + `PriceBook` (9 items) + `PlanVersion` (basePrice=99) + +`TenantSubscription` criados; 3 `UsageEvent(EXTENSION_ACTIVE_DAY)`, 1 +`UsageEvent(CALL_SECONDS, 185s)` e 4 `AIUsageRecord` (transcrição 42s, +1 análise, 1500 tokens de entrada, 600 de saída) semeados manualmente +(mesmo padrão de "transcrição semeada" já usado na PHASE 20/21, já que +gerar uma chamada real ponta a ponta não testa nada a mais do lado do +billing). `POST /billing/periods/close` fechou o período com 8 +`RatedUsageItem`s e total `R$ 101,1043129032258` — conferido a mão +(0,40 chamada + 1,451612903225806 ramal + 99 plano + 0,05 transcrição + +0,20 análise + 0,0027 tokens) e batendo exatamente. + +Confirmado também: fechar 2x o mesmo período dá 409; reabrir um período +`CLOSED` funciona e grava audit log; reabrir um período que não está +`CLOSED` dá 409; reabrir com `tenantId` de outro tenant dá 404 (RLS +isolando de verdade, não só a checagem de permission); fechar de novo +depois de reabrir reusa os `RatedUsageItem`s já existentes (não duplica) +e gera um 2º `BillingStatement` com o mesmo total; tenant admin (sem role +de plataforma) recebe 403 tentando fechar um período mas lista as +próprias `BillingStatement`s normalmente; `PriceBooksController`, +`RateDecksController`, `PlanVersionsController` (incremento de `version` +automático) e `SubscriptionsController` testados via HTTP com token real. +`runActiveDaySweep` chamado 2x seguidas no mesmo dia — confirmado não +duplicar o evento do dia (idempotência). + +**Bug real, achado no teste desta fase**: `reopenBillingPeriod` lia +`prisma.billingPeriod.findUniqueOrThrow({ where: { id } })` SEM tenant +context pra descobrir o `tenantId` do período — mas `billing_periods` tem +FORCE ROW LEVEL SECURITY, então a leitura sem `app.current_tenant_id` +nunca via a linha, e "not found" (P2025) virava 500, não um 404 de +verdade. Corrigido exigindo `tenantId` explícito no body do reopen (igual +ao close) e lendo dentro de `withTenantContext`. + +**Bug real, achado no teste desta fase**: `SubscriptionsController` +criava/lia `TenantSubscription` direto por `prisma.tenantSubscription` +sem `withTenantContext` — a tabela tem RLS, então o create batia direto +em "new row violates row-level security policy" (create) e o list sempre +voltava vazio (select). Corrigido envolvendo os dois em +`withTenantContext(prisma, tenantId, ...)`. + +**Nunca exercitado**: `rateCallByDestination`/`longestPrefixMatch` contra +um `RateDeck` real dentro de `closeBillingPeriod` (ver lacuna do +`calledNumber` acima — testado só isoladamente como função pura); +`runActiveDaySweep` rodando via `setInterval` de verdade por várias horas +(só chamado diretamente na mesma execução do teste); reajuste de preço no +meio de um período já aberto (`PriceBookItem`/`RateDeckEntry` com 2 +vigências sobrepostas). diff --git a/packages/billing/package.json b/packages/billing/package.json new file mode 100644 index 0000000..274f238 --- /dev/null +++ b/packages/billing/package.json @@ -0,0 +1,17 @@ +{ + "name": "@b2bcall/billing", + "version": "0.0.1", + "private": true, + "main": "src/index.ts", + "types": "src/index.ts", + "scripts": { + "typecheck": "tsc --noEmit", + "test": "tsx src/__tests__/rating-engine.test.ts" + }, + "dependencies": {}, + "devDependencies": { + "@types/node": "^22.20.1", + "tsx": "^4.23.12", + "typescript": "^5.7.0" + } +} diff --git a/packages/billing/src/__tests__/rating-engine.test.ts b/packages/billing/src/__tests__/rating-engine.test.ts new file mode 100644 index 0000000..1bfb79a --- /dev/null +++ b/packages/billing/src/__tests__/rating-engine.test.ts @@ -0,0 +1,143 @@ +/** + * Teste unitário do RatingEngine (agente.md secao 128-133) — matemática + * pura, sem I/O, sem banco. Roda com: + * pnpm --filter @b2bcall/billing run test + */ +import { + longestPrefixMatch, + resolvePriceBookItem, + rateCallByDestination, + rateCallFlatFallback, + rateGenericUsage, + rateActiveDaysProrated, + rateTranscriptionSeconds, + rateRecordingBytes, +} from "../rating-engine"; +import type { RateDeckEntryLike, PriceBookItemLike } from "../types"; + +function assert(condition: boolean, message: string): void { + if (!condition) { + throw new Error(`FALHOU: ${message}`); + } + console.log(`OK: ${message}`); +} + +function closeEnough(a: number, b: number, epsilon = 1e-9): boolean { + return Math.abs(a - b) < epsilon; +} + +function entry(overrides: Partial = {}): RateDeckEntryLike { + return { + id: "entry-1", + prefix: "5511", + destinationName: "SP", + destinationType: "FIXED", + pricePerMinute: 0.1, + billingIncrementSeconds: 60, + minimumSeconds: 0, + connectionFee: 0, + validFrom: new Date("2020-01-01"), + validUntil: null, + ...overrides, + }; +} + +function item(overrides: Partial = {}): PriceBookItemLike { + return { + id: "item-1", + type: "CALL_MINUTE", + unitPrice: 0.1, + effectiveFrom: new Date("2020-01-01"), + effectiveUntil: null, + ...overrides, + }; +} + +function main(): void { + // longestPrefixMatch: prefixo mais especifico vence. + { + const entries = [ + entry({ id: "generic", prefix: "55" }), + entry({ id: "sp", prefix: "5511" }), + entry({ id: "sp-mobile", prefix: "551199" }), + ]; + const match = longestPrefixMatch(entries, "5511999998888", new Date("2025-01-01")); + assert(match?.id === "sp-mobile", "longestPrefixMatch escolhe o prefixo mais longo que bate"); + } + + // longestPrefixMatch: ignora entry fora de vigencia. + { + const entries = [ + entry({ id: "expired", prefix: "5511", validFrom: new Date("2020-01-01"), validUntil: new Date("2024-01-01") }), + entry({ id: "current", prefix: "55", validFrom: new Date("2024-01-01"), validUntil: null }), + ]; + const match = longestPrefixMatch(entries, "5511999998888", new Date("2025-01-01")); + assert(match?.id === "current", "longestPrefixMatch ignora entry expirada mesmo com prefixo mais longo"); + } + + // longestPrefixMatch: sem match nenhum. + { + const match = longestPrefixMatch([entry({ prefix: "44" })], "5511999998888", new Date("2025-01-01")); + assert(match === null, "longestPrefixMatch retorna null sem nenhum prefixo batendo"); + } + + // resolvePriceBookItem: pega o effectiveFrom mais recente vigente. + { + const items = [ + item({ id: "v1", unitPrice: 0.1, effectiveFrom: new Date("2020-01-01") }), + item({ id: "v2", unitPrice: 0.2, effectiveFrom: new Date("2024-01-01") }), + ]; + const resolved = resolvePriceBookItem(items, "CALL_MINUTE", new Date("2025-01-01")); + assert(resolved?.id === "v2" && resolved.unitPrice === 0.2, "resolvePriceBookItem pega o reajuste mais novo vigente"); + } + + // rateCallByDestination: piso + arredondamento pra cima + connection fee. + { + const result = rateCallByDestination(65, entry({ pricePerMinute: 0.5, billingIncrementSeconds: 60, minimumSeconds: 30, connectionFee: 0.1 })); + // 65s >= minimo 30, arredonda pra 120 (2 incrementos de 60), 2min * 0.5 + 0.1 = 1.1 + assert(result.ratedMinutes === 2, "rateCallByDestination arredonda 65s pra 2 incrementos de 60s"); + assert(closeEnough(result.ratedAmount, 1.1), "rateCallByDestination soma connection fee ao valor por minuto"); + } + + // rateCallByDestination: minimo aplicado quando a chamada e mais curta. + { + const result = rateCallByDestination(5, entry({ pricePerMinute: 0.6, billingIncrementSeconds: 30, minimumSeconds: 30, connectionFee: 0 })); + // piso de 30s mesmo com 5s reais, arredonda pra 30 (ja multiplo de 30) + assert(closeEnough(result.ratedMinutes, 0.5), "rateCallByDestination aplica o minimo mesmo pra chamada curtissima"); + } + + // rateCallFlatFallback: sempre arredonda pro minuto cheio, sem connection fee. + { + const result = rateCallFlatFallback(185, item({ unitPrice: 0.1 })); + assert(result.ratedMinutes === 4, "rateCallFlatFallback arredonda 185s pra 4 minutos cheios"); + assert(closeEnough(result.ratedAmount, 0.4), "rateCallFlatFallback nao tem connection fee"); + assert(result.matchedEntry === null, "rateCallFlatFallback nunca referencia um RateDeckEntry"); + } + + // rateGenericUsage: multiplicacao simples. + { + assert(closeEnough(rateGenericUsage(1500, 0.000001), 0.0015), "rateGenericUsage calcula tokens de entrada"); + } + + // rateActiveDaysProrated: recurso ativo o periodo inteiro paga o preco cheio. + { + assert(closeEnough(rateActiveDaysProrated(30, 15, 30), 15), "rateActiveDaysProrated: ativo o mes inteiro paga o preco cheio"); + assert(closeEnough(rateActiveDaysProrated(15, 15, 30), 7.5), "rateActiveDaysProrated: ativo metade do periodo paga metade"); + assert(rateActiveDaysProrated(10, 15, 0) === 0, "rateActiveDaysProrated: periodo de 0 dias nao divide por zero"); + } + + // rateTranscriptionSeconds: sempre arredonda pra cima. + { + assert(closeEnough(rateTranscriptionSeconds(42, 0.05), 0.05), "rateTranscriptionSeconds arredonda 42s pro minuto cheio"); + assert(closeEnough(rateTranscriptionSeconds(61, 0.05), 0.1), "rateTranscriptionSeconds cobra 2 minutos por 61s"); + } + + // rateRecordingBytes: conversao simples bytes -> GB. + { + assert(closeEnough(rateRecordingBytes(2_000_000_000, 2), 4), "rateRecordingBytes converte 2GB corretamente"); + } + + console.log("\nTodos os testes do RatingEngine passaram."); +} + +main(); diff --git a/packages/billing/src/index.ts b/packages/billing/src/index.ts new file mode 100644 index 0000000..a3fccfa --- /dev/null +++ b/packages/billing/src/index.ts @@ -0,0 +1,11 @@ +export type { RateDeckEntryLike, PriceBookItemLike, CallRatingResult } from "./types"; +export { + longestPrefixMatch, + resolvePriceBookItem, + rateCallByDestination, + rateCallFlatFallback, + rateGenericUsage, + rateActiveDaysProrated, + rateTranscriptionSeconds, + rateRecordingBytes, +} from "./rating-engine"; diff --git a/packages/billing/src/rating-engine.ts b/packages/billing/src/rating-engine.ts new file mode 100644 index 0000000..0a2f65a --- /dev/null +++ b/packages/billing/src/rating-engine.ts @@ -0,0 +1,129 @@ +import type { RateDeckEntryLike, PriceBookItemLike, CallRatingResult } from "./types"; + +function isValidAt(validFrom: Date, validUntil: Date | null, at: Date): boolean { + return validFrom <= at && (validUntil === null || at < validUntil); +} + +/** + * Longest prefix matching (agente.md secao 129) — entre as entries do rate + * deck válidas em `at`, retorna a de prefixo mais longo que `calledNumber` + * começa com. Empate em tamanho de prefixo: indefinido qual vence (não + * deveria acontecer com um rate deck bem configurado — dois prefixos + * idênticos vigentes ao mesmo tempo é erro de cadastro, não algo pro + * engine resolver silenciosamente). + */ +export function longestPrefixMatch( + entries: RateDeckEntryLike[], + calledNumber: string, + at: Date, +): RateDeckEntryLike | null { + let best: RateDeckEntryLike | null = null; + for (const entry of entries) { + if (!isValidAt(entry.validFrom, entry.validUntil, at)) continue; + if (!calledNumber.startsWith(entry.prefix)) continue; + if (!best || entry.prefix.length > best.prefix.length) best = entry; + } + return best; +} + +/** + * Item de price book vigente em `at` pra um `type` (agente.md secao 128). + * Se mais de um item do mesmo tipo estiver vigente ao mesmo tempo (não + * deveria, mas não é validado na escrita), pega o de `effectiveFrom` mais + * recente — o reajuste mais novo vence. + */ +export function resolvePriceBookItem( + items: PriceBookItemLike[], + type: string, + at: Date, +): PriceBookItemLike | null { + let best: PriceBookItemLike | null = null; + for (const item of items) { + if (item.type !== type) continue; + if (!isValidAt(item.effectiveFrom, item.effectiveUntil, at)) continue; + if (!best || item.effectiveFrom > best.effectiveFrom) best = item; + } + return best; +} + +/** + * Chamada faturável (agente.md secao 133): aplica minimum_seconds (piso), + * arredonda PRA CIMA pro próximo múltiplo de billing_increment_seconds + * (nunca arredonda pra baixo — telecom sempre cobra o incremento cheio + * iniciado), converte pra minutos fracionários e calcula o valor + * (minutos * preço/minuto + taxa de conexão fixa). + */ +export function rateCallByDestination( + billableSeconds: number, + entry: RateDeckEntryLike, +): CallRatingResult { + const flooredSeconds = Math.max(billableSeconds, entry.minimumSeconds); + const increment = entry.billingIncrementSeconds > 0 ? entry.billingIncrementSeconds : 1; + const roundedSeconds = Math.ceil(flooredSeconds / increment) * increment; + const ratedMinutes = roundedSeconds / 60; + const ratedAmount = ratedMinutes * entry.pricePerMinute + entry.connectionFee; + + return { + matchedEntry: entry, + billingIncrementSeconds: entry.billingIncrementSeconds, + ratedMinutes, + destinationRate: entry.pricePerMinute, + ratedAmount, + }; +} + +/** + * Fallback quando nenhum prefixo do rate deck bate (secao 129 não define + * o que fazer nesse caso — decisão desta implementação: usa o + * PriceBookItem(type=CALL_MINUTE) como tarifa plana genérica, sem + * connection fee nem mínimo/incremento próprios — só arredonda pro + * minuto cheio pra cima, a granularidade mais grosseira e mais segura + * (nunca cobra a menos por falta de config). + */ +export function rateCallFlatFallback(billableSeconds: number, callMinuteItem: PriceBookItemLike): CallRatingResult { + const ratedMinutes = Math.ceil(billableSeconds / 60); + const ratedAmount = ratedMinutes * callMinuteItem.unitPrice; + + return { + matchedEntry: null, + billingIncrementSeconds: 60, + ratedMinutes, + destinationRate: callMinuteItem.unitPrice, + ratedAmount, + }; +} + +/** Uso genérico já na mesma unidade do price book item (tokens de IA, + * AI_ANALYSIS_CALL por request, etc.) — sempre quantidade * preço + * unitário, nunca calculado ad hoc em outro lugar do código (agente.md + * secao 130: "Nunca calcular billing no frontend", e por extensão, nunca + * fora deste módulo). */ +export function rateGenericUsage(quantity: number, unitPrice: number): number { + return quantity * unitPrice; +} + +/** EXTENSION_ACTIVE_DAY/AGENT_ACTIVE_DAY/TRUNK_ACTIVE_DAY → preço mensal + * (EXTENSION_MONTH/AGENT_MONTH/TRUNK_MONTH) prorateado pelos dias do + * período de billing — um recurso ativo o período inteiro paga o preço + * cheio, ativo metade do período paga metade. */ +export function rateActiveDaysProrated(activeDays: number, monthlyPrice: number, daysInPeriod: number): number { + if (daysInPeriod <= 0) return 0; + return activeDays * (monthlyPrice / daysInPeriod); +} + +/** AI_TRANSCRIPTION_SECONDS → AI_TRANSCRIPTION_MINUTE: arredonda PRA CIMA + * pro minuto cheio (mesma convenção de `rateCallByDestination` — nunca + * cobra a menos por fração de minuto). */ +export function rateTranscriptionSeconds(seconds: number, pricePerMinute: number): number { + return Math.ceil(seconds / 60) * pricePerMinute; +} + +/** RECORDING_BYTES → RECORDING_GB_MONTH. Simplificação conhecida: usa os + * bytes armazenados no momento do fechamento do período como proxy do + * consumo do mês inteiro (não faz média ponderada por dia armazenado) — + * documentado em docs/BILLING.md, aceitável nesta fase por não haver + * ainda um histórico de tamanho por dia pra fazer a média de verdade. */ +export function rateRecordingBytes(bytes: number, pricePerGbMonth: number): number { + const gigabytes = bytes / 1_000_000_000; + return gigabytes * pricePerGbMonth; +} diff --git a/packages/billing/src/types.ts b/packages/billing/src/types.ts new file mode 100644 index 0000000..d760e25 --- /dev/null +++ b/packages/billing/src/types.ts @@ -0,0 +1,34 @@ +/** + * Tipos "-Like" em vez de importar os tipos gerados do Prisma + * (`@b2bcall/database`) — o RatingEngine (agente.md secao 130) é + * matemática pura, sem I/O e sem depender do ORM. Quem chama (apps/api, + * um futuro worker de billing) passa as linhas já buscadas do banco. + */ +export interface RateDeckEntryLike { + id: string; + prefix: string; + destinationName: string; + destinationType: string; + pricePerMinute: number; + billingIncrementSeconds: number; + minimumSeconds: number; + connectionFee: number; + validFrom: Date; + validUntil: Date | null; +} + +export interface PriceBookItemLike { + id: string; + type: string; + unitPrice: number; + effectiveFrom: Date; + effectiveUntil: Date | null; +} + +export interface CallRatingResult { + matchedEntry: RateDeckEntryLike | null; + billingIncrementSeconds: number; + ratedMinutes: number; + destinationRate: number; + ratedAmount: number; +} diff --git a/packages/billing/tsconfig.json b/packages/billing/tsconfig.json new file mode 100644 index 0000000..5a24989 --- /dev/null +++ b/packages/billing/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src"] +} diff --git a/packages/database/prisma/migrations/20260828195513_billing/migration.sql b/packages/database/prisma/migrations/20260828195513_billing/migration.sql new file mode 100644 index 0000000..d2a0e0b --- /dev/null +++ b/packages/database/prisma/migrations/20260828195513_billing/migration.sql @@ -0,0 +1,321 @@ +-- CreateEnum +CREATE TYPE "tenant_subscription_status" AS ENUM ('TRIALING', 'ACTIVE', 'PAST_DUE', 'CANCELED'); + +-- CreateEnum +CREATE TYPE "price_item_type" AS ENUM ('BASE_SUBSCRIPTION', 'EXTENSION_MONTH', 'AGENT_MONTH', 'TRUNK_MONTH', 'CALL', 'CALL_MINUTE', 'FIXED_MINUTE', 'MOBILE_MINUTE', 'INTERNATIONAL_MINUTE', 'AI_TRANSCRIPTION_MINUTE', 'AI_ANALYSIS_CALL', 'AI_INPUT_TOKEN', 'AI_OUTPUT_TOKEN', 'RECORDING_GB_MONTH'); + +-- CreateEnum +CREATE TYPE "destination_type" AS ENUM ('FIXED', 'MOBILE', 'INTERNATIONAL'); + +-- CreateEnum +CREATE TYPE "usage_meter" AS ENUM ('CALL_COUNT', 'CALL_SECONDS', 'EXTENSION_ACTIVE_DAY', 'AGENT_ACTIVE_DAY', 'TRUNK_ACTIVE_DAY', 'RECORDING_BYTES', 'AI_TRANSCRIPTION_SECONDS', 'AI_ANALYSIS_REQUEST', 'AI_INPUT_TOKENS', 'AI_OUTPUT_TOKENS'); + +-- CreateEnum +CREATE TYPE "billing_period_status" AS ENUM ('OPEN', 'CALCULATING', 'READY', 'CLOSED', 'REOPENED'); + +-- CreateEnum +CREATE TYPE "billing_statement_category" AS ENUM ('PLAN_BASE', 'EXTENSIONS', 'AGENTS', 'TRUNKS', 'CALLS', 'MINUTES', 'AI_TRANSCRIPTION', 'AI_ANALYSIS', 'AI_TOKENS', 'STORAGE', 'ADJUSTMENT'); + +-- AlterTable +ALTER TABLE "calls" ADD COLUMN "billing_increment_seconds" INTEGER, +ADD COLUMN "destination_rate" DOUBLE PRECISION, +ADD COLUMN "rated_amount" DOUBLE PRECISION, +ADD COLUMN "rated_minutes" DOUBLE PRECISION; + +-- AlterTable +ALTER TABLE "tenants" ADD COLUMN "price_book_id" UUID, +ADD COLUMN "rate_deck_id" UUID; + +-- CreateTable +CREATE TABLE "plan_versions" ( + "id" UUID NOT NULL, + "plan_id" UUID NOT NULL, + "version" INTEGER NOT NULL, + "base_price" DOUBLE PRECISION NOT NULL, + "currency" TEXT NOT NULL DEFAULT 'BRL', + "effective_from" TIMESTAMP(3) NOT NULL, + "effective_until" TIMESTAMP(3), + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "plan_versions_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "tenant_subscriptions" ( + "id" UUID NOT NULL, + "tenant_id" UUID NOT NULL, + "plan_version_id" UUID NOT NULL, + "status" "tenant_subscription_status" NOT NULL DEFAULT 'ACTIVE', + "started_at" TIMESTAMP(3) NOT NULL, + "ends_at" TIMESTAMP(3), + "billing_cycle_anchor" INTEGER NOT NULL, + "currency" TEXT NOT NULL DEFAULT 'BRL', + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "tenant_subscriptions_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "price_books" ( + "id" UUID NOT NULL, + "name" TEXT NOT NULL, + "currency" TEXT NOT NULL DEFAULT 'BRL', + "is_default" BOOLEAN NOT NULL DEFAULT false, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "price_books_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "price_book_items" ( + "id" UUID NOT NULL, + "price_book_id" UUID NOT NULL, + "type" "price_item_type" NOT NULL, + "unit_price" DOUBLE PRECISION NOT NULL, + "effective_from" TIMESTAMP(3) NOT NULL, + "effective_until" TIMESTAMP(3), + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "price_book_items_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "rate_decks" ( + "id" UUID NOT NULL, + "name" TEXT NOT NULL, + "is_default" BOOLEAN NOT NULL DEFAULT false, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "rate_decks_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "rate_deck_entries" ( + "id" UUID NOT NULL, + "rate_deck_id" UUID NOT NULL, + "prefix" TEXT NOT NULL, + "destination_name" TEXT NOT NULL, + "destination_type" "destination_type" NOT NULL, + "price_per_minute" DOUBLE PRECISION NOT NULL, + "billing_increment_seconds" INTEGER NOT NULL DEFAULT 60, + "minimum_seconds" INTEGER NOT NULL DEFAULT 0, + "connection_fee" DOUBLE PRECISION NOT NULL DEFAULT 0, + "valid_from" TIMESTAMP(3) NOT NULL, + "valid_until" TIMESTAMP(3), + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "rate_deck_entries_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "usage_events" ( + "id" UUID NOT NULL, + "tenant_id" UUID NOT NULL, + "call_id" UUID, + "meter" "usage_meter" NOT NULL, + "quantity" DOUBLE PRECISION NOT NULL, + "unit" TEXT NOT NULL, + "source_type" TEXT NOT NULL, + "source_id" TEXT, + "occurred_at" TIMESTAMP(3) NOT NULL, + "metadata" JSONB, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "usage_events_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "rated_usage_items" ( + "id" UUID NOT NULL, + "tenant_id" UUID NOT NULL, + "usage_event_id" UUID, + "ai_usage_record_id" UUID, + "call_id" UUID, + "price_book_item_id" UUID, + "rate_deck_entry_id" UUID, + "quantity" DOUBLE PRECISION NOT NULL, + "unit_price" DOUBLE PRECISION NOT NULL, + "amount" DOUBLE PRECISION NOT NULL, + "currency" TEXT NOT NULL DEFAULT 'BRL', + "billing_period_id" UUID, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "rated_usage_items_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "billing_periods" ( + "id" UUID NOT NULL, + "tenant_id" UUID NOT NULL, + "period_start" TIMESTAMP(3) NOT NULL, + "period_end" TIMESTAMP(3) NOT NULL, + "status" "billing_period_status" NOT NULL DEFAULT 'OPEN', + "closed_at" TIMESTAMP(3), + "reopened_at" TIMESTAMP(3), + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "billing_periods_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "billing_statements" ( + "id" UUID NOT NULL, + "tenant_id" UUID NOT NULL, + "billing_period_id" UUID NOT NULL, + "currency" TEXT NOT NULL DEFAULT 'BRL', + "subtotal" DOUBLE PRECISION NOT NULL, + "adjustments" DOUBLE PRECISION NOT NULL DEFAULT 0, + "total" DOUBLE PRECISION NOT NULL, + "generated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "billing_statements_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "billing_statement_items" ( + "id" UUID NOT NULL, + "tenant_id" UUID NOT NULL, + "billing_statement_id" UUID NOT NULL, + "category" "billing_statement_category" NOT NULL, + "description" TEXT NOT NULL, + "quantity" DOUBLE PRECISION, + "unit_price" DOUBLE PRECISION, + "amount" DOUBLE PRECISION NOT NULL, + + CONSTRAINT "billing_statement_items_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "plan_versions_plan_id_version_key" ON "plan_versions"("plan_id", "version"); + +-- CreateIndex +CREATE INDEX "tenant_subscriptions_tenant_id_status_idx" ON "tenant_subscriptions"("tenant_id", "status"); + +-- CreateIndex +CREATE INDEX "price_book_items_price_book_id_type_effective_from_idx" ON "price_book_items"("price_book_id", "type", "effective_from"); + +-- CreateIndex +CREATE INDEX "rate_deck_entries_rate_deck_id_valid_from_idx" ON "rate_deck_entries"("rate_deck_id", "valid_from"); + +-- CreateIndex +CREATE INDEX "usage_events_tenant_id_occurred_at_idx" ON "usage_events"("tenant_id", "occurred_at"); + +-- CreateIndex +CREATE INDEX "usage_events_tenant_id_meter_idx" ON "usage_events"("tenant_id", "meter"); + +-- CreateIndex +CREATE INDEX "rated_usage_items_tenant_id_billing_period_id_idx" ON "rated_usage_items"("tenant_id", "billing_period_id"); + +-- CreateIndex +CREATE INDEX "billing_periods_tenant_id_status_idx" ON "billing_periods"("tenant_id", "status"); + +-- CreateIndex +CREATE UNIQUE INDEX "billing_periods_tenant_id_period_start_period_end_key" ON "billing_periods"("tenant_id", "period_start", "period_end"); + +-- CreateIndex +CREATE INDEX "billing_statements_tenant_id_billing_period_id_idx" ON "billing_statements"("tenant_id", "billing_period_id"); + +-- CreateIndex +CREATE INDEX "billing_statement_items_billing_statement_id_idx" ON "billing_statement_items"("billing_statement_id"); + +-- AddForeignKey +ALTER TABLE "tenants" ADD CONSTRAINT "tenants_price_book_id_fkey" FOREIGN KEY ("price_book_id") REFERENCES "price_books"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "tenants" ADD CONSTRAINT "tenants_rate_deck_id_fkey" FOREIGN KEY ("rate_deck_id") REFERENCES "rate_decks"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "plan_versions" ADD CONSTRAINT "plan_versions_plan_id_fkey" FOREIGN KEY ("plan_id") REFERENCES "plans"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "tenant_subscriptions" ADD CONSTRAINT "tenant_subscriptions_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "tenant_subscriptions" ADD CONSTRAINT "tenant_subscriptions_plan_version_id_fkey" FOREIGN KEY ("plan_version_id") REFERENCES "plan_versions"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "price_book_items" ADD CONSTRAINT "price_book_items_price_book_id_fkey" FOREIGN KEY ("price_book_id") REFERENCES "price_books"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "rate_deck_entries" ADD CONSTRAINT "rate_deck_entries_rate_deck_id_fkey" FOREIGN KEY ("rate_deck_id") REFERENCES "rate_decks"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "usage_events" ADD CONSTRAINT "usage_events_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "usage_events" ADD CONSTRAINT "usage_events_call_id_fkey" FOREIGN KEY ("call_id") REFERENCES "calls"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "rated_usage_items" ADD CONSTRAINT "rated_usage_items_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "rated_usage_items" ADD CONSTRAINT "rated_usage_items_usage_event_id_fkey" FOREIGN KEY ("usage_event_id") REFERENCES "usage_events"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "rated_usage_items" ADD CONSTRAINT "rated_usage_items_ai_usage_record_id_fkey" FOREIGN KEY ("ai_usage_record_id") REFERENCES "ai_usage_records"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "rated_usage_items" ADD CONSTRAINT "rated_usage_items_call_id_fkey" FOREIGN KEY ("call_id") REFERENCES "calls"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "rated_usage_items" ADD CONSTRAINT "rated_usage_items_price_book_item_id_fkey" FOREIGN KEY ("price_book_item_id") REFERENCES "price_book_items"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "rated_usage_items" ADD CONSTRAINT "rated_usage_items_rate_deck_entry_id_fkey" FOREIGN KEY ("rate_deck_entry_id") REFERENCES "rate_deck_entries"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "rated_usage_items" ADD CONSTRAINT "rated_usage_items_billing_period_id_fkey" FOREIGN KEY ("billing_period_id") REFERENCES "billing_periods"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "billing_periods" ADD CONSTRAINT "billing_periods_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "billing_statements" ADD CONSTRAINT "billing_statements_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "billing_statements" ADD CONSTRAINT "billing_statements_billing_period_id_fkey" FOREIGN KEY ("billing_period_id") REFERENCES "billing_periods"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "billing_statement_items" ADD CONSTRAINT "billing_statement_items_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "billing_statement_items" ADD CONSTRAINT "billing_statement_items_billing_statement_id_fkey" FOREIGN KEY ("billing_statement_id") REFERENCES "billing_statements"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- RLS (agente.md secao 233, docs/TENANT_ISOLATION.md) — mesmo padrao do +-- resto do sistema: FORCE ROW LEVEL SECURITY + policy unica baseada em +-- app.current_tenant_id. price_books/price_book_items/rate_decks/ +-- rate_deck_entries/plan_versions NAO tem RLS (catalogos globais da +-- plataforma, sem tenant_id, mesmo padrao ja usado por "plans"). +ALTER TABLE "tenant_subscriptions" ENABLE ROW LEVEL SECURITY; +ALTER TABLE "tenant_subscriptions" FORCE ROW LEVEL SECURITY; +CREATE POLICY "tenant_isolation" ON "tenant_subscriptions" + USING (tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid); + +ALTER TABLE "usage_events" ENABLE ROW LEVEL SECURITY; +ALTER TABLE "usage_events" FORCE ROW LEVEL SECURITY; +CREATE POLICY "tenant_isolation" ON "usage_events" + USING (tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid); + +ALTER TABLE "rated_usage_items" ENABLE ROW LEVEL SECURITY; +ALTER TABLE "rated_usage_items" FORCE ROW LEVEL SECURITY; +CREATE POLICY "tenant_isolation" ON "rated_usage_items" + USING (tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid); + +ALTER TABLE "billing_periods" ENABLE ROW LEVEL SECURITY; +ALTER TABLE "billing_periods" FORCE ROW LEVEL SECURITY; +CREATE POLICY "tenant_isolation" ON "billing_periods" + USING (tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid); + +ALTER TABLE "billing_statements" ENABLE ROW LEVEL SECURITY; +ALTER TABLE "billing_statements" FORCE ROW LEVEL SECURITY; +CREATE POLICY "tenant_isolation" ON "billing_statements" + USING (tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid); + +ALTER TABLE "billing_statement_items" ENABLE ROW LEVEL SECURITY; +ALTER TABLE "billing_statement_items" FORCE ROW LEVEL SECURITY; +CREATE POLICY "tenant_isolation" ON "billing_statement_items" + USING (tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid); diff --git a/packages/database/prisma/schema.prisma b/packages/database/prisma/schema.prisma index 5e43429..1d97047 100644 --- a/packages/database/prisma/schema.prisma +++ b/packages/database/prisma/schema.prisma @@ -42,11 +42,18 @@ model Tenant { telephonyDomain String? @map("telephony_domain") planId String @map("plan_id") @db.Uuid aiPrivacyLevel AIPrivacyLevel @default(AI_OFF) @map("ai_privacy_level") + // null = usa o PriceBook/RateDeck com isDefault=true (agente.md secao + // 128-129) — mesma convenção de "campo null = default/sem override" já + // usada em Queue.aiPrivacyLevel. + priceBookId String? @map("price_book_id") @db.Uuid + rateDeckId String? @map("rate_deck_id") @db.Uuid createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") deletedAt DateTime? @map("deleted_at") plan Plan @relation(fields: [planId], references: [id]) + priceBook PriceBook? @relation(fields: [priceBookId], references: [id]) + rateDeck RateDeck? @relation(fields: [rateDeckId], references: [id]) memberships TenantMembership[] userRoles UserRole[] extensions Extension[] @@ -82,6 +89,12 @@ model Tenant { aipromptVersions AIPromptVersion[] callTranscriptSegments CallTranscriptSegment[] qualityScorecardItems QualityScorecardItem[] + subscriptions TenantSubscription[] + usageEvents UsageEvent[] + ratedUsageItems RatedUsageItem[] + billingPeriods BillingPeriod[] + billingStatements BillingStatement[] + billingStatementItems BillingStatementItem[] @@map("tenants") } @@ -119,7 +132,8 @@ model Plan { createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") - tenants Tenant[] + tenants Tenant[] + planVersions PlanVersion[] @@map("plans") } @@ -1030,6 +1044,15 @@ model Call { durationSeconds Int? @map("duration_seconds") billableSeconds Int? @map("billable_seconds") + // "Chamada faturável" (agente.md secao 133) — preenchidos pelo + // RatingEngine (packages/billing) quando o UsageEvent CALL_SECONDS desta + // chamada é avaliado (nunca no momento do CDR — billableSeconds já + // existe desde a fase CDR, o resto só existe depois de rated). + billingIncrementSeconds Int? @map("billing_increment_seconds") + ratedMinutes Float? @map("rated_minutes") + destinationRate Float? @map("destination_rate") + ratedAmount Float? @map("rated_amount") + hangupCause String? @map("hangup_cause") dispositionId String? @map("disposition_id") @db.Uuid @@ -1051,6 +1074,8 @@ model Call { callAIAnalyses CallAIAnalysis[] qualityEvaluations QualityEvaluation[] aiusageRecords AIUsageRecord[] + usageEvents UsageEvent[] + ratedUsageItems RatedUsageItem[] @@index([tenantId, createdAt]) @@index([tenantId, queueId]) @@ -1550,8 +1575,9 @@ enum AIUsageType { // "AI usage metering" (secao 124) — ledger imutável (secao 233: "immutable // usage ledger > reconstruir billing de forma improvisada"), só INSERT -// pelo código da aplicação, nunca UPDATE/DELETE. Alimenta a fase Billing -// (Rating Engine), ainda não construída. +// pelo código da aplicação, nunca UPDATE/DELETE. Consumido pelo +// RatingEngine (packages/billing) junto com UsageEvent — ver comentário +// acima de UsageEvent sobre por que são 2 tabelas em vez de 1. model AIUsageRecord { id String @id @default(uuid()) @db.Uuid tenantId String @map("tenant_id") @db.Uuid @@ -1565,11 +1591,386 @@ model AIUsageRecord { occurredAt DateTime @default(now()) @map("occurred_at") - tenant Tenant @relation(fields: [tenantId], references: [id]) - call Call? @relation(fields: [callId], references: [id]) - provider AIProvider? @relation(fields: [providerId], references: [id]) + tenant Tenant @relation(fields: [tenantId], references: [id]) + call Call? @relation(fields: [callId], references: [id]) + provider AIProvider? @relation(fields: [providerId], references: [id]) + ratedUsageItems RatedUsageItem[] @@index([tenantId, occurredAt]) @@index([tenantId, type]) @@map("ai_usage_records") } + +// ============================================================ +// BILLING (agente.md secao 125-139) +// +// "Criar billing desde o início. Não tratar cobrança como relatório +// calculado posteriormente de maneira improvisada" (secao 125). +// +// PriceBook/PriceBookItem/RateDeck/RateDeckEntry/PlanVersion são +// catálogos GLOBAIS da plataforma (sem tenant_id, mesmo padrão já usado +// por `Plan` — gerenciados só pelo platform admin, um Tenant escolhe qual +// usar via `Tenant.priceBookId`/`rateDeckId`, null = o que tiver +// `isDefault=true`). TenantSubscription/UsageEvent/RatedUsageItem/ +// BillingPeriod/BillingStatement(Item) SÃO tenant-scoped, com RLS. +// ============================================================ + +// "plan_versions" (secao 126: "Preços e limites devem ser versionados"). +// Versiona só o PREÇO base da assinatura por enquanto — os limites +// (max_extensions etc.) continuam em `Plan` direto, sem versionamento +// próprio (mudam raramente nesta fase do produto; documentado como +// simplificação conhecida em docs/BILLING.md). +model PlanVersion { + id String @id @default(uuid()) @db.Uuid + planId String @map("plan_id") @db.Uuid + + version Int + basePrice Float @map("base_price") + currency String @default("BRL") + + effectiveFrom DateTime @map("effective_from") + effectiveUntil DateTime? @map("effective_until") + + createdAt DateTime @default(now()) @map("created_at") + + plan Plan @relation(fields: [planId], references: [id]) + subscriptions TenantSubscription[] + + @@unique([planId, version]) + @@map("plan_versions") +} + +enum TenantSubscriptionStatus { + TRIALING + ACTIVE + PAST_DUE + CANCELED + + @@map("tenant_subscription_status") +} + +// "tenant_subscriptions" (secao 127). +model TenantSubscription { + id String @id @default(uuid()) @db.Uuid + tenantId String @map("tenant_id") @db.Uuid + + planVersionId String @map("plan_version_id") @db.Uuid + status TenantSubscriptionStatus @default(ACTIVE) + + startedAt DateTime @map("started_at") + endsAt DateTime? @map("ends_at") + + // Dia do mês (1-28, nunca 29-31 pra evitar mês sem esse dia) em que o + // período de billing do tenant fecha (secao 127). + billingCycleAnchor Int @map("billing_cycle_anchor") + currency String @default("BRL") + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + tenant Tenant @relation(fields: [tenantId], references: [id]) + planVersion PlanVersion @relation(fields: [planVersionId], references: [id]) + + @@index([tenantId, status]) + @@map("tenant_subscriptions") +} + +enum PriceItemType { + BASE_SUBSCRIPTION + EXTENSION_MONTH + AGENT_MONTH + TRUNK_MONTH + CALL + CALL_MINUTE + FIXED_MINUTE + MOBILE_MINUTE + INTERNATIONAL_MINUTE + AI_TRANSCRIPTION_MINUTE + AI_ANALYSIS_CALL + AI_INPUT_TOKEN + AI_OUTPUT_TOKEN + RECORDING_GB_MONTH + + @@map("price_item_type") +} + +// "price_books"/"price_book_items" (secao 128) — catálogo global, +// `isDefault` marca qual usar quando `Tenant.priceBookId` é null. +model PriceBook { + id String @id @default(uuid()) @db.Uuid + name String + currency String @default("BRL") + isDefault Boolean @default(false) @map("is_default") + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + items PriceBookItem[] + tenants Tenant[] + + @@map("price_books") +} + +// Preço vigente por tipo — `validFrom`/`validUntil` permitem reajuste sem +// perder o preço histórico (o RatingEngine sempre busca o item vigente em +// `usage_event.occurred_at`, nunca "o preço de hoje" pra uso passado). +model PriceBookItem { + id String @id @default(uuid()) @db.Uuid + priceBookId String @map("price_book_id") @db.Uuid + type PriceItemType + + unitPrice Float @map("unit_price") + + effectiveFrom DateTime @map("effective_from") + effectiveUntil DateTime? @map("effective_until") + + createdAt DateTime @default(now()) @map("created_at") + + priceBook PriceBook @relation(fields: [priceBookId], references: [id]) + ratedUsageItems RatedUsageItem[] + + @@index([priceBookId, type, effectiveFrom]) + @@map("price_book_items") +} + +// "rate_decks"/"rate_deck_entries" (secao 129) — precificação por destino +// via longest prefix matching (packages/billing/src/rating-engine.ts), +// separado dos PriceBookItem(type=CALL_MINUTE) que servem só de fallback +// quando nenhum prefixo do rate deck bate com o número discado. +model RateDeck { + id String @id @default(uuid()) @db.Uuid + name String + isDefault Boolean @default(false) @map("is_default") + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + entries RateDeckEntry[] + tenants Tenant[] + + @@map("rate_decks") +} + +enum DestinationType { + FIXED + MOBILE + INTERNATIONAL + + @@map("destination_type") +} + +model RateDeckEntry { + id String @id @default(uuid()) @db.Uuid + rateDeckId String @map("rate_deck_id") @db.Uuid + + prefix String + destinationName String @map("destination_name") + destinationType DestinationType @map("destination_type") + + pricePerMinute Float @map("price_per_minute") + billingIncrementSeconds Int @default(60) @map("billing_increment_seconds") + minimumSeconds Int @default(0) @map("minimum_seconds") + connectionFee Float @default(0) @map("connection_fee") + + validFrom DateTime @map("valid_from") + validUntil DateTime? @map("valid_until") + + createdAt DateTime @default(now()) @map("created_at") + + rateDeck RateDeck @relation(fields: [rateDeckId], references: [id]) + ratedUsageItems RatedUsageItem[] + + // Longest prefix matching precisa varrer todas as entries vigentes do + // deck — sem índice em `prefix` sozinho (o match é por STARTS WITH, não + // igualdade), o RatingEngine já traz tudo pra memória por rateDeckId. + @@index([rateDeckId, validFrom]) + @@map("rate_deck_entries") +} + +enum UsageMeter { + CALL_COUNT + CALL_SECONDS + EXTENSION_ACTIVE_DAY + AGENT_ACTIVE_DAY + TRUNK_ACTIVE_DAY + RECORDING_BYTES + // Os 4 meters de IA abaixo completam a lista da secao 131, mas quem + // escreve esses eventos na prática é AIUsageRecord (ledger próprio, + // já existia desde a PHASE 20, antes da fase Billing) — o RatingEngine + // lê os dois ledgers, ver comentário em UsageEvent. Mantidos aqui só + // pra o enum bater com a especificação, não usados pra escrita. + AI_TRANSCRIPTION_SECONDS + AI_ANALYSIS_REQUEST + AI_INPUT_TOKENS + AI_OUTPUT_TOKENS + + @@map("usage_meter") +} + +// "usage_events" (secao 131) — ledger imutável, só INSERT pelo código da +// aplicação (mesma convenção de AIUsageRecord, secao 233: "immutable +// usage ledger"). Existem 2 ledgers (este + AIUsageRecord) em vez de 1 +// porque AIUsageRecord já foi construído e testado ponta a ponta na fase +// de IA, ANTES da fase Billing existir — migrar aquele código pra esta +// tabela só pra unificar seria puro churn sem ganho funcional; o +// RatingEngine simplesmente lê dos dois. Documentado em docs/BILLING.md. +model UsageEvent { + id String @id @default(uuid()) @db.Uuid + tenantId String @map("tenant_id") @db.Uuid + callId String? @map("call_id") @db.Uuid + + meter UsageMeter + quantity Float + unit String + + sourceType String @map("source_type") + sourceId String? @map("source_id") + + occurredAt DateTime @map("occurred_at") + metadata Json? + + createdAt DateTime @default(now()) @map("created_at") + + tenant Tenant @relation(fields: [tenantId], references: [id]) + call Call? @relation(fields: [callId], references: [id]) + ratedUsageItems RatedUsageItem[] + + @@index([tenantId, occurredAt]) + @@index([tenantId, meter]) + @@map("usage_events") +} + +// "rated_usage_items" (secao 132) — resultado de aplicar o RatingEngine +// num UsageEvent OU AIUsageRecord (exatamente um dos dois, checado na +// camada de serviço — Postgres não tem um jeito limpo de expressar "XOR +// de FK nullable" sem trigger, e um trigger seria over-engineering pra +// isto). `pricingVersion` referencia o PriceBookItem/RateDeckEntry usado, +// pra auditoria de qual preço vigia quando foi calculado. +model RatedUsageItem { + id String @id @default(uuid()) @db.Uuid + tenantId String @map("tenant_id") @db.Uuid + + usageEventId String? @map("usage_event_id") @db.Uuid + aiUsageRecordId String? @map("ai_usage_record_id") @db.Uuid + callId String? @map("call_id") @db.Uuid + + priceBookItemId String? @map("price_book_item_id") @db.Uuid + rateDeckEntryId String? @map("rate_deck_entry_id") @db.Uuid + + quantity Float + unitPrice Float @map("unit_price") + amount Float + currency String @default("BRL") + + billingPeriodId String? @map("billing_period_id") @db.Uuid + + createdAt DateTime @default(now()) @map("created_at") + + tenant Tenant @relation(fields: [tenantId], references: [id]) + usageEvent UsageEvent? @relation(fields: [usageEventId], references: [id]) + aiUsageRecord AIUsageRecord? @relation(fields: [aiUsageRecordId], references: [id]) + call Call? @relation(fields: [callId], references: [id]) + priceBookItem PriceBookItem? @relation(fields: [priceBookItemId], references: [id]) + rateDeckEntry RateDeckEntry? @relation(fields: [rateDeckEntryId], references: [id]) + billingPeriod BillingPeriod? @relation(fields: [billingPeriodId], references: [id]) + + @@index([tenantId, billingPeriodId]) + @@map("rated_usage_items") +} + +enum BillingPeriodStatus { + OPEN + CALCULATING + READY + CLOSED + REOPENED + + @@map("billing_period_status") +} + +// "billing_periods" (secao 134). Fechamento imutável (secao 137): depois +// de CLOSED, o service layer nunca recalcula silenciosamente — só via +// REOPEN explícito, com audit trail (recordAuditEvent, user+reason), que +// volta o status pra REOPENED (nunca direto pra OPEN, pra deixar visível +// no histórico que este período já foi fechado antes). +model BillingPeriod { + id String @id @default(uuid()) @db.Uuid + tenantId String @map("tenant_id") @db.Uuid + + periodStart DateTime @map("period_start") + periodEnd DateTime @map("period_end") + + status BillingPeriodStatus @default(OPEN) + + closedAt DateTime? @map("closed_at") + reopenedAt DateTime? @map("reopened_at") + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + tenant Tenant @relation(fields: [tenantId], references: [id]) + ratedUsageItems RatedUsageItem[] + statements BillingStatement[] + + @@unique([tenantId, periodStart, periodEnd]) + @@index([tenantId, status]) + @@map("billing_periods") +} + +enum BillingStatementCategory { + PLAN_BASE + EXTENSIONS + AGENTS + TRUNKS + CALLS + MINUTES + AI_TRANSCRIPTION + AI_ANALYSIS + AI_TOKENS + STORAGE + ADJUSTMENT + + @@map("billing_statement_category") +} + +// "billing_statements"/"billing_statement_items" (secao 135). Secao 136: +// NUNCA chamar isto de nota fiscal — só "Usage Statement"/"Billing +// Statement"/"Relatório de Consumo" (aplicado na nomenclatura da API e +// dos DTOs, não só em texto de UI que ainda não existe). +model BillingStatement { + id String @id @default(uuid()) @db.Uuid + tenantId String @map("tenant_id") @db.Uuid + billingPeriodId String @map("billing_period_id") @db.Uuid + + currency String @default("BRL") + subtotal Float + adjustments Float @default(0) + total Float + + generatedAt DateTime @default(now()) @map("generated_at") + + tenant Tenant @relation(fields: [tenantId], references: [id]) + billingPeriod BillingPeriod @relation(fields: [billingPeriodId], references: [id]) + items BillingStatementItem[] + + @@index([tenantId, billingPeriodId]) + @@map("billing_statements") +} + +model BillingStatementItem { + id String @id @default(uuid()) @db.Uuid + tenantId String @map("tenant_id") @db.Uuid + billingStatementId String @map("billing_statement_id") @db.Uuid + category BillingStatementCategory + + description String + quantity Float? + unitPrice Float? @map("unit_price") + amount Float + + tenant Tenant @relation(fields: [tenantId], references: [id]) + billingStatement BillingStatement @relation(fields: [billingStatementId], references: [id]) + + @@index([billingStatementId]) + @@map("billing_statement_items") +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 45b4261..054d2ff 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -45,6 +45,9 @@ importers: '@b2bcall/auth': specifier: workspace:* version: link:../../packages/auth + '@b2bcall/billing': + specifier: workspace:* + version: link:../../packages/billing '@b2bcall/database': specifier: workspace:* version: link:../../packages/database @@ -187,6 +190,79 @@ importers: specifier: ^5.7.0 version: 5.9.3 + apps/frontend: + dependencies: + '@radix-ui/react-avatar': + specifier: 1.1.2 + version: 1.1.2(@types/react-dom@19.1.0(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-dialog': + specifier: 1.1.4 + version: 1.1.4(@types/react-dom@19.1.0(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-dropdown-menu': + specifier: 2.1.4 + version: 2.1.4(@types/react-dom@19.1.0(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-slot': + specifier: 1.1.1 + version: 1.1.1(@types/react@19.1.0)(react@19.1.0) + '@radix-ui/react-tooltip': + specifier: 1.1.6 + version: 1.1.6(@types/react-dom@19.1.0(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@tanstack/react-query': + specifier: 5.62.7 + version: 5.62.7(react@19.1.0) + '@tanstack/react-table': + specifier: 8.20.5 + version: 8.20.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + class-variance-authority: + specifier: 0.7.1 + version: 0.7.1 + clsx: + specifier: 2.1.1 + version: 2.1.1 + lucide-react: + specifier: 0.469.0 + version: 0.469.0(react@19.1.0) + next: + specifier: 15.3.9 + version: 15.3.9(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: + specifier: 19.1.0 + version: 19.1.0 + react-dom: + specifier: 19.1.0 + version: 19.1.0(react@19.1.0) + recharts: + specifier: 2.15.0 + version: 2.15.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + server-only: + specifier: 0.0.1 + version: 0.0.1 + tailwind-merge: + specifier: 2.5.5 + version: 2.5.5 + devDependencies: + '@types/node': + specifier: ^22.20.1 + version: 22.20.1 + '@types/react': + specifier: 19.1.0 + version: 19.1.0 + '@types/react-dom': + specifier: 19.1.0 + version: 19.1.0(@types/react@19.1.0) + autoprefixer: + specifier: 10.4.20 + version: 10.4.20(postcss@8.4.49) + postcss: + specifier: 8.4.49 + version: 8.4.49 + tailwindcss: + specifier: 3.4.17 + version: 3.4.17 + typescript: + specifier: ^5.7.0 + version: 5.9.3 + apps/predictive-dialer: dependencies: '@b2bcall/database': @@ -243,6 +319,18 @@ importers: specifier: ^5.7.0 version: 5.9.3 + packages/billing: + devDependencies: + '@types/node': + specifier: ^22.20.1 + version: 22.20.1 + tsx: + specifier: ^4.23.12 + version: 4.23.12 + typescript: + specifier: ^5.7.0 + version: 5.9.3 + packages/database: dependencies: '@prisma/adapter-pg': @@ -250,7 +338,7 @@ importers: version: 7.10.0 '@prisma/client': specifier: ^7.10.0 - version: 7.10.0(prisma@7.10.0(@types/react@19.2.18)(magicast@0.5.4)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@5.9.3))(typescript@5.9.3) + version: 7.10.0(prisma@7.10.0(@types/react-dom@19.1.0(@types/react@19.2.18))(@types/react@19.2.18)(magicast@0.5.4)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@5.9.3))(typescript@5.9.3) pg: specifier: ^8.23.0 version: 8.23.0 @@ -260,7 +348,7 @@ importers: version: 8.23.1 prisma: specifier: 7.10.0 - version: 7.10.0(@types/react@19.2.18)(magicast@0.5.4)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@5.9.3) + version: 7.10.0(@types/react-dom@19.1.0(@types/react@19.2.18))(@types/react@19.2.18)(magicast@0.5.4)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@5.9.3) tsx: specifier: ^4.23.12 version: 4.23.12 @@ -322,6 +410,10 @@ importers: packages: + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + '@aws-sdk/checksums@3.1000.29': resolution: {integrity: sha512-Dtu0gr4dnATZAPwEYbpCsG+MpLM7OAliy2gTepEFQwl1vZ6DL3QMH2FveMa3HLvPsOdhJsPRB3KtxVhph9T75A==} engines: {node: '>=20.0.0'} @@ -407,6 +499,10 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + engines: {node: '>=6.9.0'} + '@babel/types@7.29.8': resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==} engines: {node: '>=6.9.0'} @@ -428,6 +524,9 @@ packages: '@electric-sql/pglite@0.4.3': resolution: {integrity: sha512-ichuWTgtd4mOM1G4SpyGJa5trT03lWbMypDV0fUXUCXg5hiHqVAz/bZyV68NqmkLB7WcYmj1RMJVSp8HV/v/ZQ==} + '@emnapi/runtime@1.11.3': + resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==} + '@esbuild/aix-ppc64@0.28.2': resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} engines: {node: '>=18'} @@ -617,9 +716,190 @@ packages: '@fastify/rate-limit@11.2.0': resolution: {integrity: sha512-X7osJd4XSvMoejYrnJkSZYYjY1eNYoBqhjlzf1RakC2204qExFqZFTKj5+T7VuzA/iUI9Z3UoSqQRkB2HpG0oQ==} + '@floating-ui/core@1.8.0': + resolution: {integrity: sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==} + + '@floating-ui/dom@1.8.0': + resolution: {integrity: sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==} + + '@floating-ui/react-dom@2.1.9': + resolution: {integrity: sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.12': + resolution: {integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==} + + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + '@ioredis/commands@2.0.0': resolution: {integrity: sha512-vrx0AE/T0h7cRZwfo1M39Cr+ZhZrkf0V8mQN75wucKCxCLD9l/VX6no3gFvrLqD1IlG/1LtzWovqEw3t0Vr9zg==} + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@lukeed/csprng@1.1.0': resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} engines: {node: '>=8'} @@ -691,6 +971,61 @@ packages: '@nestjs/platform-socket.io': optional: true + '@next/env@15.3.9': + resolution: {integrity: sha512-I7wMCjlHc85EvAebNYJCRBZ+shdrGhcIXBviWmDzGYXwTQ+WrYpfg1LBOnTK1Bn3b+ud5apesNObXKEGqi/C3g==} + + '@next/swc-darwin-arm64@15.3.5': + resolution: {integrity: sha512-lM/8tilIsqBq+2nq9kbTW19vfwFve0NR7MxfkuSUbRSgXlMQoJYg+31+++XwKVSXk4uT23G2eF/7BRIKdn8t8w==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@next/swc-darwin-x64@15.3.5': + resolution: {integrity: sha512-WhwegPQJ5IfoUNZUVsI9TRAlKpjGVK0tpJTL6KeiC4cux9774NYE9Wu/iCfIkL/5J8rPAkqZpG7n+EfiAfidXA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@next/swc-linux-arm64-gnu@15.3.5': + resolution: {integrity: sha512-LVD6uMOZ7XePg3KWYdGuzuvVboxujGjbcuP2jsPAN3MnLdLoZUXKRc6ixxfs03RH7qBdEHCZjyLP/jBdCJVRJQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@next/swc-linux-arm64-musl@15.3.5': + resolution: {integrity: sha512-k8aVScYZ++BnS2P69ClK7v4nOu702jcF9AIHKu6llhHEtBSmM2zkPGl9yoqbSU/657IIIb0QHpdxEr0iW9z53A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@next/swc-linux-x64-gnu@15.3.5': + resolution: {integrity: sha512-2xYU0DI9DGN/bAHzVwADid22ba5d/xrbrQlr2U+/Q5WkFUzeL0TDR963BdrtLS/4bMmKZGptLeg6282H/S2i8A==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@next/swc-linux-x64-musl@15.3.5': + resolution: {integrity: sha512-TRYIqAGf1KCbuAB0gjhdn5Ytd8fV+wJSM2Nh2is/xEqR8PZHxfQuaiNhoF50XfY90sNpaRMaGhF6E+qjV1b9Tg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@next/swc-win32-arm64-msvc@15.3.5': + resolution: {integrity: sha512-h04/7iMEUSMY6fDGCvdanKqlO1qYvzNxntZlCzfE8i5P0uqzVQWQquU1TIhlz0VqGQGXLrFDuTJVONpqGqjGKQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@next/swc-win32-x64-msvc@15.3.5': + resolution: {integrity: sha512-5fhH6fccXxnX2KhllnGhkYMndhOiLOLEiVGYjP2nizqeGWkN10sA9taATlXwake2E2XMvYZjjz0Uj7T0y+z1yw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + '@nodable/entities@3.0.0': resolution: {integrity: sha512-8L9xFeTYKhm49xfIypoe2W5wV1m/3Z58kT+7kR9A8OyFxcPduI4VmxaUMQyKYrRjUoLLSXv6EKKID5Tvj9cUVw==} @@ -780,6 +1115,18 @@ packages: resolution: {integrity: sha512-VBOWfM2u58/to3DFqTGJ2U5cJKQwmjN2zxzsQNZ5a2o8Z6aUrhvqQh8NdgotIF1Y0tMsBNtzOBDBdfvvkwJDSQ==} engines: {node: '>= 10'} + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + '@pinojs/redact@0.4.0': resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} @@ -846,9 +1193,60 @@ packages: react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 + '@radix-ui/primitive@1.1.1': + resolution: {integrity: sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==} + '@radix-ui/primitive@1.1.3': resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} + '@radix-ui/react-arrow@1.1.1': + resolution: {integrity: sha512-NaVpZfmv8SKeZbn4ijN2V3jlHA9ngBG16VnIIm22nUR0Yk8KUALyBxT3KYEUnNuch9sTE8UTsS3whzBgKOL30w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-avatar@1.1.2': + resolution: {integrity: sha512-GaC7bXQZ5VgZvVvsJ5mu/AEbjYLnhhkoidOboC50Z6FFlLA03wG2ianUoH+zgDQ31/9gCF59bE4+2bBgTyMiig==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collection@1.1.1': + resolution: {integrity: sha512-LwT3pSho9Dljg+wY2KN2mrrh6y3qELfftINERIzBUO9e0N+t0oMTyn3k9iv+ZqgrwGkRnLpNJrsMv9BZlt2yuA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-compose-refs@1.1.1': + resolution: {integrity: sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-compose-refs@1.1.2': resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} peerDependencies: @@ -858,6 +1256,159 @@ packages: '@types/react': optional: true + '@radix-ui/react-context@1.1.1': + resolution: {integrity: sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dialog@1.1.4': + resolution: {integrity: sha512-Ur7EV1IwQGCyaAuyDRiOLA5JIUZxELJljF+MbM/2NC0BYwfuRrbpS30BiQBJrVruscgUkieKkqXYDOoByaxIoA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-direction@1.1.0': + resolution: {integrity: sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dismissable-layer@1.1.3': + resolution: {integrity: sha512-onrWn/72lQoEucDmJnr8uczSNTujT0vJnA/X5+3AkChVPowr8n1yvIKIabhWyMQeMvvmdpsvcyDqx3X1LEXCPg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-dropdown-menu@2.1.4': + resolution: {integrity: sha512-iXU1Ab5ecM+yEepGAWK8ZhMyKX4ubFdCNtol4sT9D0OVErG9PNElfx3TQhjw7n7BC5nFVz68/5//clWy+8TXzA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-focus-guards@1.1.1': + resolution: {integrity: sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-focus-scope@1.1.1': + resolution: {integrity: sha512-01omzJAYRxXdG2/he/+xy+c8a8gCydoQ1yOxnWNcRhrrBW5W+RQJ22EK1SaO8tb3WoUsuEw7mJjBozPzihDFjA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-id@1.1.0': + resolution: {integrity: sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-menu@2.1.4': + resolution: {integrity: sha512-BnOgVoL6YYdHAG6DtXONaR29Eq4nvbi8rutrV/xlr3RQCMMb3yqP85Qiw/3NReozrSW+4dfLkK+rc1hb4wPU/A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popper@1.2.1': + resolution: {integrity: sha512-3kn5Me69L+jv82EKRuQCXdYyf1DqHwD2U/sxoNgBGCB7K9TRc3bQamQ+5EPM9EvyPdli0W41sROd+ZU1dTCztw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-portal@1.1.3': + resolution: {integrity: sha512-NciRqhXnGojhT93RPyDaMPfLH3ZSl4jjIFbZQ1b/vxvZEdHsBZ49wP9w8L3HzUQwep01LcWtkUvm0OVB5JAHTw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-presence@1.1.2': + resolution: {integrity: sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.0.1': + resolution: {integrity: sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-primitive@2.1.3': resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} peerDependencies: @@ -871,6 +1422,28 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-roving-focus@1.1.1': + resolution: {integrity: sha512-QE1RoxPGJ/Nm8Qmk0PxP8ojmoaS67i0s7hVssS7KuI2FQoc/uzVlZsqKfQvxPE6D8hICCPHJ4D88zNhT3OOmkw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slot@1.1.1': + resolution: {integrity: sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-slot@1.2.3': resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} peerDependencies: @@ -893,6 +1466,37 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-tooltip@1.1.6': + resolution: {integrity: sha512-TLB5D8QLExS1uDn7+wH/bjEmRurNMTzNrtq7IjaS4kjion9NtzsTGkvR5+i7yc9q01Pi2KMM2cN3f8UG4IvvXA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-use-callback-ref@1.1.0': + resolution: {integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-controllable-state@1.1.0': + resolution: {integrity: sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-use-controllable-state@1.2.2': resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} peerDependencies: @@ -911,6 +1515,24 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-escape-keydown@1.1.0': + resolution: {integrity: sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.0': + resolution: {integrity: sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-use-layout-effect@1.1.1': resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} peerDependencies: @@ -920,6 +1542,40 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-rect@1.1.0': + resolution: {integrity: sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-size@1.1.0': + resolution: {integrity: sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-visually-hidden@1.1.1': + resolution: {integrity: sha512-vVfA2IZ9q/J+gEamvj761Oq1FpWgCDaNOOIfbPVp2MVPLEomUr5+Vf7kJGwQ24YxZSlQVar7Bes8kyTo5Dshpg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/rect@1.1.0': + resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==} + '@smithy/core@3.33.3': resolution: {integrity: sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg==} engines: {node: '>=18.0.0'} @@ -950,6 +1606,31 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@swc/counter@0.1.3': + resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + + '@swc/helpers@0.5.15': + resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + + '@tanstack/query-core@5.62.7': + resolution: {integrity: sha512-fgpfmwatsrUal6V+8EC2cxZIQVl9xvL7qYa03gsdsCy985UTUlS4N+/3hCzwR0PclYDqisca2AqR1BVgJGpUDA==} + + '@tanstack/react-query@5.62.7': + resolution: {integrity: sha512-+xCtP4UAFDTlRTYyEjLx0sRtWyr5GIk7TZjZwBu4YaNahi3Rt2oMyRqfpfVrtwsqY2sayP4iXVCwmC+ZqqFmuw==} + peerDependencies: + react: ^18 || ^19 + + '@tanstack/react-table@8.20.5': + resolution: {integrity: sha512-WEHopKw3znbUZ61s9i0+i9g8drmDo6asTWbrQh8Us63DAk/M0FkmIqERew6P71HI75ksZ2Pxyuf4vvKh9rAkiA==} + engines: {node: '>=12'} + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' + + '@tanstack/table-core@8.20.5': + resolution: {integrity: sha512-P9dF7XbibHph2PFRz8gfBKEXEY/HJPOhym8CHmjF8y3q5mWpKx9xtZapXQUWCgkqvsK0R46Azuz+VaxD4Xl+Tg==} + engines: {node: '>=12'} + '@tokenizer/inflate@0.4.1': resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==} engines: {node: '>=18'} @@ -969,6 +1650,9 @@ packages: '@types/d3-delaunay@6.0.1': resolution: {integrity: sha512-tLxQ2sfT0p6sxdG75c6f/ekqxjyYR0+LwPrsO1mbC9YDBzPJhs2HbJJRrn8Ez1DBoHRo2yx7YEATI+8V1nGMnQ==} + '@types/d3-ease@3.0.2': + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + '@types/d3-format@3.0.1': resolution: {integrity: sha512-5KY70ifCCzorkLuIkDe0Z9YTf9RR2CjBX1iaJG+rgM/cPP+sO+q9YdQ9WdhQcgPj1EQiJ2/0+yUkkziTG6Lubg==} @@ -993,6 +1677,9 @@ packages: '@types/d3-time@3.0.0': resolution: {integrity: sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg==} + '@types/d3-timer@3.0.2': + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + '@types/geojson@7946.0.16': resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} @@ -1005,6 +1692,14 @@ packages: '@types/pg@8.23.1': resolution: {integrity: sha512-fKVHpikPdg4GKks3JuLEhvwSyvwzF23hnabPy6DD8ljVbC7+6J5dQzdv4arV6jqq57djnMgs1HKBxX4P8aBI3A==} + '@types/react-dom@19.1.0': + resolution: {integrity: sha512-21E2zejNNRtjG4hKIyJz4aWswGEcNFTgttA0bZIRGjj1HA/tbSUxIJnIcYbn98pwJck0cS1bsQhn6eaKqbcFWw==} + peerDependencies: + '@types/react': ^19.0.0 + + '@types/react@19.1.0': + resolution: {integrity: sha512-UaicktuQI+9UKyA4njtDOGBD/67t8YEBt2xdfqu8+gP9hqPUPsiXlNPcpS2gVdjmis5GKPG3fCxbQLVgxsQZ8w==} + '@types/react@19.2.18': resolution: {integrity: sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==} @@ -1067,13 +1762,34 @@ packages: ajv@8.20.0: resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + anynum@1.0.1: resolution: {integrity: sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==} + arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + + aria-hidden@1.2.6: + resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} + engines: {node: '>=10'} + atomic-sleep@1.0.0: resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} engines: {node: '>=8.0.0'} + autoprefixer@10.4.20: + resolution: {integrity: sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + avvio@9.3.0: resolution: {integrity: sha512-g2tQ7LE7oOSqDfwEm3M+ZCMTJc7KiZCdJ4UwyZJb5ckTKyYu50OYmvv0mCFXPuYXoM4zkSt8zM9XQ9KCvxA74A==} @@ -1085,12 +1801,34 @@ packages: resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==} engines: {node: ^4.5.0 || >= 5.9} + baseline-browser-mapping@2.11.19: + resolution: {integrity: sha512-Grytf1xOxOEMTGRwx6rLGKkTabd4vMg3VrKdj/7joCmV0qgh4QwMMO6xh34YEXQqirAuUdgQGa5orJQQ+69RBw==} + engines: {node: '>=6.0.0'} + hasBin: true + better-result@2.10.0: resolution: {integrity: sha512-oQhh0y1qo2/ZKdAAEvHZAqKKiHOFU5k/bW96fE2ScgQOVkJRiHwB+nOS1SgFsYqRlxMDWvefXi9Q3px7QvgNDw==} + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + bowser@2.14.1: resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.28.8: + resolution: {integrity: sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + busboy@1.6.0: + resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} + engines: {node: '>=10.16.0'} + c12@3.3.4: resolution: {integrity: sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA==} peerDependencies: @@ -1099,6 +1837,17 @@ packages: magicast: optional: true + camelcase-css@2.0.1: + resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} + engines: {node: '>= 6'} + + caniuse-lite@1.0.30001810: + resolution: {integrity: sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + chokidar@5.0.0: resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} engines: {node: '>= 20.19.0'} @@ -1109,13 +1858,27 @@ packages: class-validator@0.15.1: resolution: {integrity: sha512-LqoS80HBBSCVhz/3KloUly0ovokxpdOLR++Al3J3+dHXWt9sTKlKd4eYtoxhxyUjoe5+UcIM+5k9MIxyBWnRTw==} + class-variance-authority@0.7.1: + resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + classnames@2.5.1: resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} + client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + cluster-key-slot@1.1.1: resolution: {integrity: sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw==} engines: {node: '>=0.10.0'} + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + confbox@0.2.4: resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==} @@ -1135,6 +1898,11 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} @@ -1154,6 +1922,10 @@ packages: resolution: {integrity: sha512-IMLNldruDQScrcfT+MWnazhHbDJhcRJyOEBAJfwQnHle1RPh6WDuLvxNArUju2VSMSUuKlY5BGHRJ2cYyoFLQQ==} engines: {node: '>=12'} + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + d3-format@3.1.0: resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==} engines: {node: '>=12'} @@ -1186,6 +1958,10 @@ packages: resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} engines: {node: '>=12'} + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -1195,6 +1971,9 @@ packages: supports-color: optional: true + decimal.js-light@2.5.1: + resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + deepmerge-ts@7.1.5: resolution: {integrity: sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==} engines: {node: '>=16.0.0'} @@ -1216,6 +1995,22 @@ packages: destr@2.0.5: resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + detect-node-es@1.1.0: + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + + didyoumean@1.2.2: + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + + dlv@1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + + dom-helpers@5.2.1: + resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + dotenv@17.4.2: resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} engines: {node: '>=12'} @@ -1223,6 +2018,9 @@ packages: effect@3.20.0: resolution: {integrity: sha512-qMLfDJscrNG8p/aw+IkT9W7fgj50Z4wG5bLBy0Txsxz8iUHjDIkOgO3SV0WZfnQbNG2VJYb0b+rDLMrhM4+Krw==} + electron-to-chromium@1.5.415: + resolution: {integrity: sha512-958V+Kbhtgz+SxXeEVKBjrlKRBIDAYvUJfwhjxMZ5S6ut9jAl7l9ZKBkBrvjyjZE36PabLUo2L8kEeV5O4vgJg==} + elkjs@0.11.1: resolution: {integrity: sha512-zxxR9k+rx5ktMwT/FwyLdPCrq7xN6e4VGGHH8hA01vVYKjTFik7nHOxBnAYtrgYUB1RpAiLvA1/U2YraWxyKKg==} @@ -1242,14 +2040,25 @@ packages: resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + esbuild@0.28.2: resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} engines: {node: '>=18'} hasBin: true + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + esl@11.2.1: resolution: {integrity: sha512-H1qQHYbSgZ61yzzPh29HPFCoZC63ZkJ9t4YJJPlKmkqmNPUbeVCzM5dW6GzPkbm+6KIyejW2sXGegj8cqfzhhw==} + eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + exsolve@1.1.1: resolution: {integrity: sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g==} @@ -1263,6 +2072,14 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-equals@5.4.1: + resolution: {integrity: sha512-DjlFSM5Pk9cGcL0q5QXl66eGzx0N6szNgaswwc5ZphlBohjTVJSnGgI+rJVOgOi65qUoQnDZN4nDqi33udtydQ==} + engines: {node: '>=6.0.0'} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + fast-json-stringify@7.0.1: resolution: {integrity: sha512-eRSayARSbbwlBjpP4vnTTIRD5QPcIrmihPxDeN1DtKnHPg66UuJLx+8hlK1kaFdjvzyQ/dzALoi4vwAQ+T+iZA==} @@ -1297,10 +2114,23 @@ packages: fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + file-type@22.0.2: resolution: {integrity: sha512-0H8TsCUGBLx+V5adH3EY52hTAcyLKbV1D4gq5cIOJ6DnQAHeV9Z2Hhuc5CoBX4YmvB2oL+JIC84z0qO7JsCoNw==} engines: {node: '>=22'} + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + find-my-way@9.7.0: resolution: {integrity: sha512-f2JHn75x2JlwUwLenZypgczR7YWMb/uO9BvUXtus+JMgkbIkLADd38cI4EiV+OQqrGo1Zlq6V8wnqMJ8e62wUQ==} engines: {node: '>=20'} @@ -1313,14 +2143,24 @@ packages: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} + fraction.js@4.3.7: + resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + generate-function@2.3.1: resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==} + get-nonce@1.0.1: + resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} + engines: {node: '>=6'} + get-port-please@3.2.0: resolution: {integrity: sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==} @@ -1328,6 +2168,14 @@ packages: resolution: {integrity: sha512-r+mvuDjrjMpsdw46Kmeydb8bdHm7wOKw8wNBtTndkjbPjgAp5oUJUxRE76wZFknxIPokfWvep2qSXK37aXE6zg==} hasBin: true + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -1337,6 +2185,10 @@ packages: graphmatch@1.1.1: resolution: {integrity: sha512-5ykVn/EXM1hF0XCaWh05VbYvEiOL2lY1kBxZtaYsyvjp7cmWOU1XsAdfQBwClraEofXDT197lFbXOEVMHpvQOg==} + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + helmet@8.3.0: resolution: {integrity: sha512-Qgpiaws3Sm30Av8Eah6sjMCZZwjlBu+E68rhpCWBshY1lb09HtLwj5GviX0OyQIn+ulUS0iX0AxN5n3tLZzz1w==} engines: {node: '>=18.0.0'} @@ -1364,6 +2216,26 @@ packages: resolution: {integrity: sha512-aq+t5NAc+cS6rZQQVWC2x98CPqGtKKTMDd4Gaodv0wShnItdKg/51djkGJ1hqH+Oy0ivDftCbSLCQob8zso01w==} engines: {node: '>= 10'} + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-core-module@2.16.2: + resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + is-property@1.0.2: resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==} @@ -1377,6 +2249,10 @@ packages: resolution: {integrity: sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==} engines: {node: '>=6'} + jiti@1.21.7: + resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} + hasBin: true + jiti@2.7.0: resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true @@ -1384,6 +2260,9 @@ packages: jose@6.2.10: resolution: {integrity: sha512-iiW7J9qRFlGxvCOIBDBDxFePQSn7ZMAnrYGhrrOo6siO/MIqwfyilLR27pkfDgUk+raLuzADS8A3S/KLBisc0g==} + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + json-schema-ref-resolver@3.0.0: resolution: {integrity: sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==} @@ -1396,6 +2275,13 @@ packages: light-my-request@6.6.0: resolution: {integrity: sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==} + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + load-esm@1.0.3: resolution: {integrity: sha512-v5xlu8eHD1+6r8EHTg6hfmO97LN8ugKtiXcy5e6oN72iD2r6u0RPfLl6fxM+7Wnh2ZRq15o0russMst44WauPA==} engines: {node: '>=13.2.0'} @@ -1406,13 +2292,30 @@ packages: long@5.3.2: resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + lru.min@1.1.4: resolution: {integrity: sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==} engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'} + lucide-react@0.469.0: + resolution: {integrity: sha512-28vvUnnKQ/dBwiCQtwJw7QauYnE7yd2Cyp4tTTJpvglX4EMpbflcdBgrgToX2j71B3YvugK/NH3BGUk+E/p/Fw==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + magicast@0.5.4: resolution: {integrity: sha512-llBEhWm1SacoRwgHUoQJYtwp4PBLF4faQi5TCpIGyGs9n4y5+juI0tDgyKIfpqxckRHaHzouUEph3THklWh03w==} + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + mime-db@1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} @@ -1428,14 +2331,55 @@ packages: resolution: {integrity: sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==} engines: {node: '>= 8.0'} + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + named-placeholders@1.1.6: resolution: {integrity: sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==} engines: {node: '>=8.0.0'} + nanoid@3.3.18: + resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + negotiator@0.6.3: resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} engines: {node: '>= 0.6'} + next@15.3.9: + resolution: {integrity: sha512-bat50ogkh2esjfkbqmVocL5QunR9RGCSO2oQKFjKeDcEylIgw3JY6CMfGnzoVfXJ9SDLHI546sHmsk90D2ivwQ==} + engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@playwright/test': ^1.41.2 + babel-plugin-react-compiler: '*' + react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@playwright/test': + optional: true + babel-plugin-react-compiler: + optional: true + sass: + optional: true + + node-releases@2.0.54: + resolution: {integrity: sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==} + engines: {node: '>=18'} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + normalize-range@0.1.2: + resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} + engines: {node: '>=0.10.0'} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -1459,6 +2403,9 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + path-to-regexp@8.4.2: resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} @@ -1502,6 +2449,17 @@ packages: pgpass@1.0.5: resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} + engines: {node: '>=12'} + pino-abstract-transport@3.0.0: resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==} @@ -1512,9 +2470,58 @@ packages: resolution: {integrity: sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==} hasBin: true + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + pkg-types@2.3.1: resolution: {integrity: sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==} + postcss-import@15.1.0: + resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} + engines: {node: '>=14.0.0'} + peerDependencies: + postcss: ^8.0.0 + + postcss-js@4.1.0: + resolution: {integrity: sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==} + engines: {node: ^12 || ^14 || >= 16} + peerDependencies: + postcss: ^8.4.21 + + postcss-load-config@4.0.2: + resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} + engines: {node: '>= 14'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + + postcss-nested@6.2.0: + resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.2.14 + + postcss-selector-parser@6.1.4: + resolution: {integrity: sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==} + engines: {node: '>=4'} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.4.31: + resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} + engines: {node: ^10 || ^12 || >=14} + + postcss@8.4.49: + resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==} + engines: {node: ^10 || ^12 || >=14} + postgres-array@2.0.0: resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} engines: {node: '>=4'} @@ -1558,27 +2565,97 @@ packages: process-warning@5.1.0: resolution: {integrity: sha512-jQSaVHsPgtyw60e1rQ/A+/ArPEj/S8pS/vFnyGa/gYFXrKk/6RuDkoqVDQ5NI5MmS01698ltlAk0NoDBNLujRw==} + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + proper-lockfile@4.1.2: resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} pure-rand@6.1.0: resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + quick-format-unescaped@4.0.4: resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} rc9@3.0.1: resolution: {integrity: sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ==} + react-dom@19.1.0: + resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==} + peerDependencies: + react: ^19.1.0 + react-dom@19.2.8: resolution: {integrity: sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==} peerDependencies: react: ^19.2.8 + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + + react-remove-scroll-bar@2.3.8: + resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-remove-scroll@2.7.2: + resolution: {integrity: sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react-smooth@4.0.4: + resolution: {integrity: sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + react-style-singleton@2.2.3: + resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react-transition-group@4.4.5: + resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} + peerDependencies: + react: '>=16.6.0' + react-dom: '>=16.6.0' + + react@19.1.0: + resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==} + engines: {node: '>=0.10.0'} + react@19.2.8: resolution: {integrity: sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==} engines: {node: '>=0.10.0'} + read-cache@1.0.2: + resolution: {integrity: sha512-/peqiBB/n07gQGLsWaHho3WfvUyRscw0gYTsEFMhrIe/nWLkYaf5SbKYjGYqtRV3aPwykJgF2VEMo1ac4bnsGA==} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + readdirp@5.1.1: resolution: {integrity: sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==} engines: {node: '>= 20.19.0'} @@ -1590,6 +2667,17 @@ packages: real-require@1.0.0: resolution: {integrity: sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==} + recharts-scale@0.4.5: + resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==} + + recharts@2.15.0: + resolution: {integrity: sha512-cIvMxDfpAmqAmVgc4yb7pgm/O1tmmkl/CjrvXuW+62/+7jj/iF9Ykm+hb/UJt42TREHMyd3gb+pkgoa2MxgDIw==} + engines: {node: '>=14'} + deprecated: 1.x and 2.x branches are no longer active. Bump to Recharts v3 to receive latest features and bugfixes. See https://github.com/recharts/recharts/wiki/3.0-migration-guide + peerDependencies: + react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + redis-errors@1.2.0: resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} engines: {node: '>=4'} @@ -1604,6 +2692,11 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} + resolve@1.22.12: + resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} + engines: {node: '>= 0.4'} + hasBin: true + ret@0.5.0: resolution: {integrity: sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==} engines: {node: '>=10'} @@ -1622,6 +2715,9 @@ packages: robust-predicates@3.0.3: resolution: {integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==} + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + rxjs@7.8.2: resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} @@ -1636,6 +2732,9 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + scheduler@0.26.0: + resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} + scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} @@ -1650,9 +2749,16 @@ packages: seq-queue@0.0.5: resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==} + server-only@0.0.1: + resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==} + set-cookie-parser@2.7.2: resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -1700,6 +2806,10 @@ packages: std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + streamsearch@1.1.0: + resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} + engines: {node: '>=10.0.0'} + strnum@2.4.2: resolution: {integrity: sha512-rDG3Ah4TV0k1hWvLSzkZtMmLN9+eS+h3knq4MP6A42Y3Yh5qGNnOUs1jJkoSr8FG5dsL28c7KgkIBzSEykqtuw==} @@ -1707,10 +2817,58 @@ packages: resolution: {integrity: sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==} engines: {node: '>=18'} + styled-jsx@5.1.6: + resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true + + sucrase@3.35.1: + resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + tailwind-merge@2.5.5: + resolution: {integrity: sha512-0LXunzzAZzo0tEPxV3I297ffKZPlKDrjj7NXphC8V5ak9yHC5zRmxnOe2m/Rd/7ivsOMJe3JZ2JVocoDdQTRBA==} + + tailwindcss@3.4.17: + resolution: {integrity: sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==} + engines: {node: '>=14.0.0'} + hasBin: true + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + thread-stream@4.2.0: resolution: {integrity: sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==} engines: {node: '>=20'} + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + toad-cache@3.7.4: resolution: {integrity: sha512-m1TdR/rvT7kgGJZhspNtXdsdYk0fddFpJJFlG5s+UkPFo6lkLoZ3YLOaovPYjq1R75NP5JfeTlSHaOsE09peCg==} engines: {node: '>=20'} @@ -1719,6 +2877,9 @@ packages: resolution: {integrity: sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==} engines: {node: '>=14.16'} + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -1743,6 +2904,35 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + update-browserslist-db@1.3.2: + resolution: {integrity: sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + use-callback-ref@1.3.3: + resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-sidecar@1.1.3: + resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + valibot@1.4.2: resolution: {integrity: sha512-gjdCvJ6d3RyHAneqxMYMW9QMCwYMb3jpOO0IyHZV1bnRHFBHrX3VkIILt5XYR0WhwHiH7Mty8ovuPZ/O3gamrg==} peerDependencies: @@ -1759,6 +2949,9 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + victory-vendor@36.9.2: + resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -1784,11 +2977,18 @@ packages: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} + yaml@2.9.0: + resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} + engines: {node: '>= 14.6'} + hasBin: true + zeptomatch@2.1.0: resolution: {integrity: sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA==} snapshots: + '@alloc/quick-lru@5.2.0': {} + '@aws-sdk/checksums@3.1000.29': dependencies: '@aws-sdk/core': 3.977.9 @@ -1965,6 +3165,8 @@ snapshots: '@babel/types': 7.29.8 optional: true + '@babel/runtime@7.29.7': {} + '@babel/types@7.29.8': dependencies: '@babel/helper-string-parser': 7.29.7 @@ -1983,6 +3185,11 @@ snapshots: '@electric-sql/pglite@0.4.3': {} + '@emnapi/runtime@1.11.3': + dependencies: + tslib: 2.8.1 + optional: true + '@esbuild/aix-ppc64@0.28.2': optional: true @@ -2111,8 +3318,136 @@ snapshots: ip-address: 10.5.0 toad-cache: 3.7.4 + '@floating-ui/core@1.8.0': + dependencies: + '@floating-ui/utils': 0.2.12 + + '@floating-ui/dom@1.8.0': + dependencies: + '@floating-ui/core': 1.8.0 + '@floating-ui/utils': 0.2.12 + + '@floating-ui/react-dom@2.1.9(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@floating-ui/dom': 1.8.0 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + + '@floating-ui/utils@0.2.12': {} + + '@img/colour@1.1.0': + optional: true + + '@img/sharp-darwin-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.4 + optional: true + + '@img/sharp-darwin-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true + + '@img/sharp-linux-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.4 + optional: true + + '@img/sharp-linux-arm@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.4 + optional: true + + '@img/sharp-linux-ppc64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.4 + optional: true + + '@img/sharp-linux-riscv64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.2.4 + optional: true + + '@img/sharp-linux-s390x@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.4 + optional: true + + '@img/sharp-linux-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + optional: true + + '@img/sharp-wasm32@0.34.5': + dependencies: + '@emnapi/runtime': 1.11.3 + optional: true + + '@img/sharp-win32-arm64@0.34.5': + optional: true + + '@img/sharp-win32-ia32@0.34.5': + optional: true + + '@img/sharp-win32-x64@0.34.5': + optional: true + '@ioredis/commands@2.0.0': {} + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + '@lukeed/csprng@1.1.0': {} '@lukeed/ms@2.0.2': {} @@ -2185,6 +3520,32 @@ snapshots: optionalDependencies: '@nestjs/platform-socket.io': 12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@12.0.1)(rxjs@7.8.2) + '@next/env@15.3.9': {} + + '@next/swc-darwin-arm64@15.3.5': + optional: true + + '@next/swc-darwin-x64@15.3.5': + optional: true + + '@next/swc-linux-arm64-gnu@15.3.5': + optional: true + + '@next/swc-linux-arm64-musl@15.3.5': + optional: true + + '@next/swc-linux-x64-gnu@15.3.5': + optional: true + + '@next/swc-linux-x64-musl@15.3.5': + optional: true + + '@next/swc-win32-arm64-msvc@15.3.5': + optional: true + + '@next/swc-win32-x64-msvc@15.3.5': + optional: true + '@nodable/entities@3.0.0': {} '@node-rs/argon2-android-arm-eabi@2.1.0': @@ -2242,6 +3603,18 @@ snapshots: '@node-rs/argon2-win32-ia32-msvc': 2.1.0 '@node-rs/argon2-win32-x64-msvc': 2.1.0 + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + '@pinojs/redact@0.4.0': {} '@prisma/adapter-pg@7.10.0': @@ -2255,11 +3628,11 @@ snapshots: '@prisma/client-runtime-utils@7.10.0': {} - '@prisma/client@7.10.0(prisma@7.10.0(@types/react@19.2.18)(magicast@0.5.4)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@5.9.3))(typescript@5.9.3)': + '@prisma/client@7.10.0(prisma@7.10.0(@types/react-dom@19.1.0(@types/react@19.2.18))(@types/react@19.2.18)(magicast@0.5.4)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@5.9.3))(typescript@5.9.3)': dependencies: '@prisma/client-runtime-utils': 7.10.0 optionalDependencies: - prisma: 7.10.0(@types/react@19.2.18)(magicast@0.5.4)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@5.9.3) + prisma: 7.10.0(@types/react-dom@19.1.0(@types/react@19.2.18))(@types/react@19.2.18)(magicast@0.5.4)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@5.9.3) typescript: 5.9.3 '@prisma/config@7.10.0(magicast@0.5.4)': @@ -2331,9 +3704,9 @@ snapshots: env-paths: 3.0.0 proper-lockfile: 4.1.2 - '@prisma/studio-core@0.33.0(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@prisma/studio-core@0.33.0(@types/react-dom@19.1.0(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/react-toggle': 1.1.10(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-toggle': 1.1.10(@types/react-dom@19.1.0(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@types/react': 19.2.18 '@visx/curve': 4.0.1-alpha.0 '@visx/event': 4.0.1-alpha.0 @@ -2350,21 +3723,246 @@ snapshots: transitivePeerDependencies: - '@types/react-dom' + '@radix-ui/primitive@1.1.1': {} + '@radix-ui/primitive@1.1.3': {} + '@radix-ui/react-arrow@1.1.1(@types/react-dom@19.1.0(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@19.1.0(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.0 + '@types/react-dom': 19.1.0(@types/react@19.1.0) + + '@radix-ui/react-avatar@1.1.2(@types/react-dom@19.1.0(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-context': 1.1.1(@types/react@19.1.0)(react@19.1.0) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@19.1.0(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.1.0)(react@19.1.0) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.1.0)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.0 + '@types/react-dom': 19.1.0(@types/react@19.1.0) + + '@radix-ui/react-collection@1.1.1(@types/react-dom@19.1.0(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.1.0)(react@19.1.0) + '@radix-ui/react-context': 1.1.1(@types/react@19.1.0)(react@19.1.0) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@19.1.0(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-slot': 1.1.1(@types/react@19.1.0)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.0 + '@types/react-dom': 19.1.0(@types/react@19.1.0) + + '@radix-ui/react-compose-refs@1.1.1(@types/react@19.1.0)(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.0 + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.18)(react@19.2.8)': dependencies: react: 19.2.8 optionalDependencies: '@types/react': 19.2.18 - '@radix-ui/react-primitive@2.1.3(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@radix-ui/react-context@1.1.1(@types/react@19.1.0)(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.0 + + '@radix-ui/react-dialog@1.1.4(@types/react-dom@19.1.0(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.1.0)(react@19.1.0) + '@radix-ui/react-context': 1.1.1(@types/react@19.1.0)(react@19.1.0) + '@radix-ui/react-dismissable-layer': 1.1.3(@types/react-dom@19.1.0(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-focus-guards': 1.1.1(@types/react@19.1.0)(react@19.1.0) + '@radix-ui/react-focus-scope': 1.1.1(@types/react-dom@19.1.0(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-id': 1.1.0(@types/react@19.1.0)(react@19.1.0) + '@radix-ui/react-portal': 1.1.3(@types/react-dom@19.1.0(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-presence': 1.1.2(@types/react-dom@19.1.0(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@19.1.0(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-slot': 1.1.1(@types/react@19.1.0)(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@19.1.0)(react@19.1.0) + aria-hidden: 1.2.6 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react-remove-scroll: 2.7.2(@types/react@19.1.0)(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.0 + '@types/react-dom': 19.1.0(@types/react@19.1.0) + + '@radix-ui/react-direction@1.1.0(@types/react@19.1.0)(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.0 + + '@radix-ui/react-dismissable-layer@1.1.3(@types/react-dom@19.1.0(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.1.0)(react@19.1.0) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@19.1.0(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.1.0)(react@19.1.0) + '@radix-ui/react-use-escape-keydown': 1.1.0(@types/react@19.1.0)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.0 + '@types/react-dom': 19.1.0(@types/react@19.1.0) + + '@radix-ui/react-dropdown-menu@2.1.4(@types/react-dom@19.1.0(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.1.0)(react@19.1.0) + '@radix-ui/react-context': 1.1.1(@types/react@19.1.0)(react@19.1.0) + '@radix-ui/react-id': 1.1.0(@types/react@19.1.0)(react@19.1.0) + '@radix-ui/react-menu': 2.1.4(@types/react-dom@19.1.0(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@19.1.0(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@19.1.0)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.0 + '@types/react-dom': 19.1.0(@types/react@19.1.0) + + '@radix-ui/react-focus-guards@1.1.1(@types/react@19.1.0)(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.0 + + '@radix-ui/react-focus-scope@1.1.1(@types/react-dom@19.1.0(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.1.0)(react@19.1.0) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@19.1.0(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.1.0)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.0 + '@types/react-dom': 19.1.0(@types/react@19.1.0) + + '@radix-ui/react-id@1.1.0(@types/react@19.1.0)(react@19.1.0)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.1.0)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.0 + + '@radix-ui/react-menu@2.1.4(@types/react-dom@19.1.0(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-collection': 1.1.1(@types/react-dom@19.1.0(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.1.0)(react@19.1.0) + '@radix-ui/react-context': 1.1.1(@types/react@19.1.0)(react@19.1.0) + '@radix-ui/react-direction': 1.1.0(@types/react@19.1.0)(react@19.1.0) + '@radix-ui/react-dismissable-layer': 1.1.3(@types/react-dom@19.1.0(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-focus-guards': 1.1.1(@types/react@19.1.0)(react@19.1.0) + '@radix-ui/react-focus-scope': 1.1.1(@types/react-dom@19.1.0(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-id': 1.1.0(@types/react@19.1.0)(react@19.1.0) + '@radix-ui/react-popper': 1.2.1(@types/react-dom@19.1.0(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-portal': 1.1.3(@types/react-dom@19.1.0(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-presence': 1.1.2(@types/react-dom@19.1.0(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@19.1.0(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-roving-focus': 1.1.1(@types/react-dom@19.1.0(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-slot': 1.1.1(@types/react@19.1.0)(react@19.1.0) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.1.0)(react@19.1.0) + aria-hidden: 1.2.6 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react-remove-scroll: 2.7.2(@types/react@19.1.0)(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.0 + '@types/react-dom': 19.1.0(@types/react@19.1.0) + + '@radix-ui/react-popper@1.2.1(@types/react-dom@19.1.0(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@floating-ui/react-dom': 2.1.9(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-arrow': 1.1.1(@types/react-dom@19.1.0(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.1.0)(react@19.1.0) + '@radix-ui/react-context': 1.1.1(@types/react@19.1.0)(react@19.1.0) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@19.1.0(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.1.0)(react@19.1.0) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.1.0)(react@19.1.0) + '@radix-ui/react-use-rect': 1.1.0(@types/react@19.1.0)(react@19.1.0) + '@radix-ui/react-use-size': 1.1.0(@types/react@19.1.0)(react@19.1.0) + '@radix-ui/rect': 1.1.0 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.0 + '@types/react-dom': 19.1.0(@types/react@19.1.0) + + '@radix-ui/react-portal@1.1.3(@types/react-dom@19.1.0(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@19.1.0(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.1.0)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.0 + '@types/react-dom': 19.1.0(@types/react@19.1.0) + + '@radix-ui/react-presence@1.1.2(@types/react-dom@19.1.0(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.1.0)(react@19.1.0) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.1.0)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.0 + '@types/react-dom': 19.1.0(@types/react@19.1.0) + + '@radix-ui/react-primitive@2.0.1(@types/react-dom@19.1.0(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-slot': 1.1.1(@types/react@19.1.0)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.0 + '@types/react-dom': 19.1.0(@types/react@19.1.0) + + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.1.0(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: '@radix-ui/react-slot': 1.2.3(@types/react@19.2.18)(react@19.2.8) react: 19.2.8 react-dom: 19.2.8(react@19.2.8) optionalDependencies: '@types/react': 19.2.18 + '@types/react-dom': 19.1.0(@types/react@19.2.18) + + '@radix-ui/react-roving-focus@1.1.1(@types/react-dom@19.1.0(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-collection': 1.1.1(@types/react-dom@19.1.0(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.1.0)(react@19.1.0) + '@radix-ui/react-context': 1.1.1(@types/react@19.1.0)(react@19.1.0) + '@radix-ui/react-direction': 1.1.0(@types/react@19.1.0)(react@19.1.0) + '@radix-ui/react-id': 1.1.0(@types/react@19.1.0)(react@19.1.0) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@19.1.0(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.1.0)(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@19.1.0)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.0 + '@types/react-dom': 19.1.0(@types/react@19.1.0) + + '@radix-ui/react-slot@1.1.1(@types/react@19.1.0)(react@19.1.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.1.0)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.0 '@radix-ui/react-slot@1.2.3(@types/react@19.2.18)(react@19.2.8)': dependencies: @@ -2373,15 +3971,49 @@ snapshots: optionalDependencies: '@types/react': 19.2.18 - '@radix-ui/react-toggle@1.1.10(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@radix-ui/react-toggle@1.1.10(@types/react-dom@19.1.0(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-primitive': 2.1.3(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.0(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.18)(react@19.2.8) react: 19.2.8 react-dom: 19.2.8(react@19.2.8) optionalDependencies: '@types/react': 19.2.18 + '@types/react-dom': 19.1.0(@types/react@19.2.18) + + '@radix-ui/react-tooltip@1.1.6(@types/react-dom@19.1.0(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.1.0)(react@19.1.0) + '@radix-ui/react-context': 1.1.1(@types/react@19.1.0)(react@19.1.0) + '@radix-ui/react-dismissable-layer': 1.1.3(@types/react-dom@19.1.0(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-id': 1.1.0(@types/react@19.1.0)(react@19.1.0) + '@radix-ui/react-popper': 1.2.1(@types/react-dom@19.1.0(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-portal': 1.1.3(@types/react-dom@19.1.0(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-presence': 1.1.2(@types/react-dom@19.1.0(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@19.1.0(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-slot': 1.1.1(@types/react@19.1.0)(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@19.1.0)(react@19.1.0) + '@radix-ui/react-visually-hidden': 1.1.1(@types/react-dom@19.1.0(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.0 + '@types/react-dom': 19.1.0(@types/react@19.1.0) + + '@radix-ui/react-use-callback-ref@1.1.0(@types/react@19.1.0)(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.0 + + '@radix-ui/react-use-controllable-state@1.1.0(@types/react@19.1.0)(react@19.1.0)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.1.0)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.0 '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.18)(react@19.2.8)': dependencies: @@ -2398,12 +4030,50 @@ snapshots: optionalDependencies: '@types/react': 19.2.18 + '@radix-ui/react-use-escape-keydown@1.1.0(@types/react@19.1.0)(react@19.1.0)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.1.0)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.0 + + '@radix-ui/react-use-layout-effect@1.1.0(@types/react@19.1.0)(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.0 + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.18)(react@19.2.8)': dependencies: react: 19.2.8 optionalDependencies: '@types/react': 19.2.18 + '@radix-ui/react-use-rect@1.1.0(@types/react@19.1.0)(react@19.1.0)': + dependencies: + '@radix-ui/rect': 1.1.0 + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.0 + + '@radix-ui/react-use-size@1.1.0(@types/react@19.1.0)(react@19.1.0)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.1.0)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.0 + + '@radix-ui/react-visually-hidden@1.1.1(@types/react-dom@19.1.0(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@19.1.0(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.0 + '@types/react-dom': 19.1.0(@types/react@19.1.0) + + '@radix-ui/rect@1.1.0': {} + '@smithy/core@3.33.3': dependencies: '@smithy/types': 4.17.2 @@ -2441,6 +4111,27 @@ snapshots: '@standard-schema/spec@1.1.0': {} + '@swc/counter@0.1.3': {} + + '@swc/helpers@0.5.15': + dependencies: + tslib: 2.8.1 + + '@tanstack/query-core@5.62.7': {} + + '@tanstack/react-query@5.62.7(react@19.1.0)': + dependencies: + '@tanstack/query-core': 5.62.7 + react: 19.1.0 + + '@tanstack/react-table@8.20.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@tanstack/table-core': 8.20.5 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + + '@tanstack/table-core@8.20.5': {} + '@tokenizer/inflate@0.4.1': dependencies: debug: 4.4.3 @@ -2460,6 +4151,8 @@ snapshots: '@types/d3-delaunay@6.0.1': {} + '@types/d3-ease@3.0.2': {} + '@types/d3-format@3.0.1': {} '@types/d3-geo@3.1.0': @@ -2484,6 +4177,8 @@ snapshots: '@types/d3-time@3.0.0': {} + '@types/d3-timer@3.0.2': {} + '@types/geojson@7946.0.16': {} '@types/lodash@4.17.25': {} @@ -2498,6 +4193,19 @@ snapshots: pg-protocol: 1.16.0 pg-types: 2.2.0 + '@types/react-dom@19.1.0(@types/react@19.1.0)': + dependencies: + '@types/react': 19.1.0 + + '@types/react-dom@19.1.0(@types/react@19.2.18)': + dependencies: + '@types/react': 19.2.18 + optional: true + + '@types/react@19.1.0': + dependencies: + csstype: 3.2.3 + '@types/react@19.2.18': dependencies: csstype: 3.2.3 @@ -2603,10 +4311,33 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 + any-promise@1.3.0: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.2 + anynum@1.0.1: {} + arg@5.0.2: {} + + aria-hidden@1.2.6: + dependencies: + tslib: 2.8.1 + atomic-sleep@1.0.0: {} + autoprefixer@10.4.20(postcss@8.4.49): + dependencies: + browserslist: 4.28.8 + caniuse-lite: 1.0.30001810 + fraction.js: 4.3.7 + normalize-range: 0.1.2 + picocolors: 1.1.1 + postcss: 8.4.49 + postcss-value-parser: 4.2.0 + avvio@9.3.0: dependencies: '@fastify/error': 4.2.0 @@ -2616,10 +4347,30 @@ snapshots: base64id@2.0.0: {} + baseline-browser-mapping@2.11.19: {} + better-result@2.10.0: {} + binary-extensions@2.3.0: {} + bowser@2.14.1: {} + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.28.8: + dependencies: + baseline-browser-mapping: 2.11.19 + caniuse-lite: 1.0.30001810 + electron-to-chromium: 1.5.415 + node-releases: 2.0.54 + update-browserslist-db: 1.3.2(browserslist@4.28.8) + + busboy@1.6.0: + dependencies: + streamsearch: 1.1.0 + c12@3.3.4(magicast@0.5.4): dependencies: chokidar: 5.0.0 @@ -2637,6 +4388,22 @@ snapshots: optionalDependencies: magicast: 0.5.4 + camelcase-css@2.0.1: {} + + caniuse-lite@1.0.30001810: {} + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + chokidar@5.0.0: dependencies: readdirp: 5.1.1 @@ -2649,10 +4416,20 @@ snapshots: libphonenumber-js: 1.13.11 validator: 13.15.35 + class-variance-authority@0.7.1: + dependencies: + clsx: 2.1.1 + classnames@2.5.1: {} + client-only@0.0.1: {} + + clsx@2.1.1: {} + cluster-key-slot@1.1.1: {} + commander@4.1.1: {} + confbox@0.2.4: {} cookie@0.7.2: {} @@ -2670,6 +4447,8 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + cssesc@3.0.0: {} + csstype@3.2.3: {} d3-array@3.2.1: @@ -2686,6 +4465,8 @@ snapshots: dependencies: delaunator: 5.1.0 + d3-ease@3.0.1: {} + d3-format@3.1.0: {} d3-geo@3.1.0: @@ -2718,10 +4499,14 @@ snapshots: dependencies: d3-array: 3.2.4 + d3-timer@3.0.1: {} + debug@4.4.3: dependencies: ms: 2.1.3 + decimal.js-light@2.5.1: {} + deepmerge-ts@7.1.5: {} defu@6.1.7: {} @@ -2736,6 +4521,20 @@ snapshots: destr@2.0.5: {} + detect-libc@2.1.2: + optional: true + + detect-node-es@1.1.0: {} + + didyoumean@1.2.2: {} + + dlv@1.1.3: {} + + dom-helpers@5.2.1: + dependencies: + '@babel/runtime': 7.29.7 + csstype: 3.2.3 + dotenv@17.4.2: {} effect@3.20.0: @@ -2743,6 +4542,8 @@ snapshots: '@standard-schema/spec': 1.1.0 fast-check: 3.23.2 + electron-to-chromium@1.5.415: {} + elkjs@0.11.1: {} empathic@2.0.0: {} @@ -2768,6 +4569,8 @@ snapshots: env-paths@3.0.0: {} + es-errors@1.3.0: {} + esbuild@0.28.2: optionalDependencies: '@esbuild/aix-ppc64': 0.28.2 @@ -2797,8 +4600,12 @@ snapshots: '@esbuild/win32-ia32': 0.28.2 '@esbuild/win32-x64': 0.28.2 + escalade@3.2.0: {} + esl@11.2.1: {} + eventemitter3@4.0.7: {} + exsolve@1.1.1: {} fast-check@3.23.2: @@ -2809,6 +4616,16 @@ snapshots: fast-deep-equal@3.1.3: {} + fast-equals@5.4.1: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + fast-json-stringify@7.0.1: dependencies: '@fastify/merge-json-schemas': 0.2.1 @@ -2868,6 +4685,10 @@ snapshots: dependencies: reusify: 1.1.0 + fdir@6.5.0(picomatch@4.0.7): + optionalDependencies: + picomatch: 4.0.7 + file-type@22.0.2: dependencies: '@tokenizer/inflate': 0.4.1 @@ -2877,6 +4698,10 @@ snapshots: transitivePeerDependencies: - supports-color + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + find-my-way@9.7.0: dependencies: fast-deep-equal: 3.1.3 @@ -2894,23 +4719,41 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 + fraction.js@4.3.7: {} + fsevents@2.3.3: optional: true + function-bind@1.1.2: {} + generate-function@2.3.1: dependencies: is-property: 1.0.2 + get-nonce@1.0.1: {} + get-port-please@3.2.0: {} giget@3.3.1: {} + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + graceful-fs@4.2.11: {} grammex@3.1.13: {} graphmatch@1.1.1: {} + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + helmet@8.3.0: {} iconv-lite@0.7.3: @@ -2936,6 +4779,22 @@ snapshots: ipaddr.js@2.5.0: {} + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-core-module@2.16.2: + dependencies: + hasown: 2.0.4 + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + is-property@1.0.2: {} is-unsafe@2.0.2: {} @@ -2944,10 +4803,14 @@ snapshots: iterare@1.2.1: {} + jiti@1.21.7: {} + jiti@2.7.0: {} jose@6.2.10: {} + js-tokens@4.0.0: {} + json-schema-ref-resolver@3.0.0: dependencies: dequal: 2.0.3 @@ -2962,14 +4825,26 @@ snapshots: process-warning: 4.0.1 set-cookie-parser: 2.7.2 + lilconfig@3.1.3: {} + + lines-and-columns@1.2.4: {} + load-esm@1.0.3: {} lodash@4.17.21: {} long@5.3.2: {} + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + lru.min@1.1.4: {} + lucide-react@0.469.0(react@19.1.0): + dependencies: + react: 19.1.0 + magicast@0.5.4: dependencies: '@babel/parser': 7.29.8 @@ -2977,6 +4852,13 @@ snapshots: source-map-js: 1.2.1 optional: true + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + mime-db@1.52.0: {} mime-types@2.1.35: @@ -2997,12 +4879,51 @@ snapshots: seq-queue: 0.0.5 sqlstring: 2.3.3 + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + named-placeholders@1.1.6: dependencies: lru.min: 1.1.4 + nanoid@3.3.18: {} + negotiator@0.6.3: {} + next@15.3.9(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + '@next/env': 15.3.9 + '@swc/counter': 0.1.3 + '@swc/helpers': 0.5.15 + busboy: 1.6.0 + caniuse-lite: 1.0.30001810 + postcss: 8.4.31 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + styled-jsx: 5.1.6(react@19.1.0) + optionalDependencies: + '@next/swc-darwin-arm64': 15.3.5 + '@next/swc-darwin-x64': 15.3.5 + '@next/swc-linux-arm64-gnu': 15.3.5 + '@next/swc-linux-arm64-musl': 15.3.5 + '@next/swc-linux-x64-gnu': 15.3.5 + '@next/swc-linux-x64-musl': 15.3.5 + '@next/swc-win32-arm64-msvc': 15.3.5 + '@next/swc-win32-x64-msvc': 15.3.5 + sharp: 0.34.5 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + + node-releases@2.0.54: {} + + normalize-path@3.0.0: {} + + normalize-range@0.1.2: {} + object-assign@4.1.1: {} object-hash@3.0.0: {} @@ -3015,6 +4936,8 @@ snapshots: path-key@3.1.1: {} + path-parse@1.0.7: {} + path-to-regexp@8.4.2: {} pathe@2.0.3: {} @@ -3056,6 +4979,12 @@ snapshots: dependencies: split2: 4.2.0 + picocolors@1.1.1: {} + + picomatch@2.3.2: {} + + picomatch@4.0.7: {} + pino-abstract-transport@3.0.0: dependencies: split2: 4.2.0 @@ -3076,12 +5005,57 @@ snapshots: sonic-boom: 4.2.1 thread-stream: 4.2.0 + pirates@4.0.7: {} + pkg-types@2.3.1: dependencies: confbox: 0.2.4 exsolve: 1.1.1 pathe: 2.0.3 + postcss-import@15.1.0(postcss@8.4.49): + dependencies: + postcss: 8.4.49 + postcss-value-parser: 4.2.0 + read-cache: 1.0.2 + resolve: 1.22.12 + + postcss-js@4.1.0(postcss@8.4.49): + dependencies: + camelcase-css: 2.0.1 + postcss: 8.4.49 + + postcss-load-config@4.0.2(postcss@8.4.49): + dependencies: + lilconfig: 3.1.3 + yaml: 2.9.0 + optionalDependencies: + postcss: 8.4.49 + + postcss-nested@6.2.0(postcss@8.4.49): + dependencies: + postcss: 8.4.49 + postcss-selector-parser: 6.1.4 + + postcss-selector-parser@6.1.4: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-value-parser@4.2.0: {} + + postcss@8.4.31: + dependencies: + nanoid: 3.3.18 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postcss@8.4.49: + dependencies: + nanoid: 3.3.18 + picocolors: 1.1.1 + source-map-js: 1.2.1 + postgres-array@2.0.0: {} postgres-array@3.0.4: {} @@ -3096,12 +5070,12 @@ snapshots: postgres@3.4.7: {} - prisma@7.10.0(@types/react@19.2.18)(magicast@0.5.4)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@5.9.3): + prisma@7.10.0(@types/react-dom@19.1.0(@types/react@19.2.18))(@types/react@19.2.18)(magicast@0.5.4)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@5.9.3): dependencies: '@prisma/config': 7.10.0(magicast@0.5.4) '@prisma/dev': 0.24.17(typescript@5.9.3) '@prisma/engines': 7.10.0 - '@prisma/studio-core': 0.33.0(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@prisma/studio-core': 0.33.0(@types/react-dom@19.1.0(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) mysql2: 3.15.3 postgres: 3.4.7 optionalDependencies: @@ -3117,6 +5091,12 @@ snapshots: process-warning@5.1.0: {} + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + proper-lockfile@4.1.2: dependencies: graceful-fs: 4.2.11 @@ -3125,6 +5105,8 @@ snapshots: pure-rand@6.1.0: {} + queue-microtask@1.2.3: {} + quick-format-unescaped@4.0.4: {} rc9@3.0.1: @@ -3132,19 +5114,97 @@ snapshots: defu: 6.1.7 destr: 2.0.5 + react-dom@19.1.0(react@19.1.0): + dependencies: + react: 19.1.0 + scheduler: 0.26.0 + react-dom@19.2.8(react@19.2.8): dependencies: react: 19.2.8 scheduler: 0.27.0 + react-is@16.13.1: {} + + react-is@18.3.1: {} + + react-remove-scroll-bar@2.3.8(@types/react@19.1.0)(react@19.1.0): + dependencies: + react: 19.1.0 + react-style-singleton: 2.2.3(@types/react@19.1.0)(react@19.1.0) + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.1.0 + + react-remove-scroll@2.7.2(@types/react@19.1.0)(react@19.1.0): + dependencies: + react: 19.1.0 + react-remove-scroll-bar: 2.3.8(@types/react@19.1.0)(react@19.1.0) + react-style-singleton: 2.2.3(@types/react@19.1.0)(react@19.1.0) + tslib: 2.8.1 + use-callback-ref: 1.3.3(@types/react@19.1.0)(react@19.1.0) + use-sidecar: 1.1.3(@types/react@19.1.0)(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.0 + + react-smooth@4.0.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + fast-equals: 5.4.1 + prop-types: 15.8.1 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react-transition-group: 4.4.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + + react-style-singleton@2.2.3(@types/react@19.1.0)(react@19.1.0): + dependencies: + get-nonce: 1.0.1 + react: 19.1.0 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.1.0 + + react-transition-group@4.4.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + '@babel/runtime': 7.29.7 + dom-helpers: 5.2.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + + react@19.1.0: {} + react@19.2.8: {} + read-cache@1.0.2: {} + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.2 + readdirp@5.1.1: {} real-require@0.2.0: {} real-require@1.0.0: {} + recharts-scale@0.4.5: + dependencies: + decimal.js-light: 2.5.1 + + recharts@2.15.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + clsx: 2.1.1 + eventemitter3: 4.0.7 + lodash: 4.17.21 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react-is: 18.3.1 + react-smooth: 4.0.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + recharts-scale: 0.4.5 + tiny-invariant: 1.3.3 + victory-vendor: 36.9.2 + redis-errors@1.2.0: {} reflect-metadata@0.2.2: {} @@ -3153,6 +5213,13 @@ snapshots: require-from-string@2.0.2: {} + resolve@1.22.12: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.16.2 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + ret@0.5.0: {} retry@0.12.0: {} @@ -3163,6 +5230,10 @@ snapshots: robust-predicates@3.0.3: {} + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + rxjs@7.8.2: dependencies: tslib: 2.8.1 @@ -3175,6 +5246,8 @@ snapshots: safer-buffer@2.1.2: {} + scheduler@0.26.0: {} + scheduler@0.27.0: {} secure-json-parse@4.1.0: {} @@ -3183,8 +5256,42 @@ snapshots: seq-queue@0.0.5: {} + server-only@0.0.1: {} + set-cookie-parser@2.7.2: {} + sharp@0.34.5: + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.8.5 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.5 + '@img/sharp-darwin-x64': 0.34.5 + '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-linux-arm': 0.34.5 + '@img/sharp-linux-arm64': 0.34.5 + '@img/sharp-linux-ppc64': 0.34.5 + '@img/sharp-linux-riscv64': 0.34.5 + '@img/sharp-linux-s390x': 0.34.5 + '@img/sharp-linux-x64': 0.34.5 + '@img/sharp-linuxmusl-arm64': 0.34.5 + '@img/sharp-linuxmusl-x64': 0.34.5 + '@img/sharp-wasm32': 0.34.5 + '@img/sharp-win32-arm64': 0.34.5 + '@img/sharp-win32-ia32': 0.34.5 + '@img/sharp-win32-x64': 0.34.5 + optional: true + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -3229,8 +5336,7 @@ snapshots: dependencies: atomic-sleep: 1.0.0 - source-map-js@1.2.1: - optional: true + source-map-js@1.2.1: {} split2@4.2.0: {} @@ -3240,6 +5346,8 @@ snapshots: std-env@3.10.0: {} + streamsearch@1.1.0: {} + strnum@2.4.2: dependencies: anynum: 1.0.1 @@ -3248,10 +5356,75 @@ snapshots: dependencies: '@tokenizer/token': 0.3.0 + styled-jsx@5.1.6(react@19.1.0): + dependencies: + client-only: 0.0.1 + react: 19.1.0 + + sucrase@3.35.1: + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + commander: 4.1.1 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.7 + tinyglobby: 0.2.17 + ts-interface-checker: 0.1.13 + + supports-preserve-symlinks-flag@1.0.0: {} + + tailwind-merge@2.5.5: {} + + tailwindcss@3.4.17: + dependencies: + '@alloc/quick-lru': 5.2.0 + arg: 5.0.2 + chokidar: 3.6.0 + didyoumean: 1.2.2 + dlv: 1.1.3 + fast-glob: 3.3.3 + glob-parent: 6.0.2 + is-glob: 4.0.3 + jiti: 1.21.7 + lilconfig: 3.1.3 + micromatch: 4.0.8 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.1.1 + postcss: 8.4.49 + postcss-import: 15.1.0(postcss@8.4.49) + postcss-js: 4.1.0(postcss@8.4.49) + postcss-load-config: 4.0.2(postcss@8.4.49) + postcss-nested: 6.2.0(postcss@8.4.49) + postcss-selector-parser: 6.1.4 + resolve: 1.22.12 + sucrase: 3.35.1 + transitivePeerDependencies: + - ts-node + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + thread-stream@4.2.0: dependencies: real-require: 1.0.0 + tiny-invariant@1.3.3: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + toad-cache@3.7.4: {} token-types@6.1.2: @@ -3260,6 +5433,8 @@ snapshots: '@tokenizer/token': 0.3.0 ieee754: 1.2.1 + ts-interface-checker@0.1.13: {} + tslib@2.8.1: {} tsx@4.23.12: @@ -3278,6 +5453,29 @@ snapshots: undici-types@6.21.0: {} + update-browserslist-db@1.3.2(browserslist@4.28.8): + dependencies: + browserslist: 4.28.8 + escalade: 3.2.0 + picocolors: 1.1.1 + + use-callback-ref@1.3.3(@types/react@19.1.0)(react@19.1.0): + dependencies: + react: 19.1.0 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.1.0 + + use-sidecar@1.1.3(@types/react@19.1.0)(react@19.1.0): + dependencies: + detect-node-es: 1.1.0 + react: 19.1.0 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.1.0 + + util-deprecate@1.0.2: {} + valibot@1.4.2(typescript@5.9.3): optionalDependencies: typescript: 5.9.3 @@ -3286,6 +5484,23 @@ snapshots: vary@1.1.2: {} + victory-vendor@36.9.2: + dependencies: + '@types/d3-array': 3.0.3 + '@types/d3-ease': 3.0.2 + '@types/d3-interpolate': 3.0.1 + '@types/d3-scale': 4.0.2 + '@types/d3-shape': 3.1.7 + '@types/d3-time': 3.0.0 + '@types/d3-timer': 3.0.2 + d3-array: 3.2.4 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-scale: 4.0.2 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-timer: 3.0.1 + which@2.0.2: dependencies: isexe: 2.0.0 @@ -3296,6 +5511,8 @@ snapshots: xtend@4.0.2: {} + yaml@2.9.0: {} + zeptomatch@2.1.0: dependencies: grammex: 3.1.13 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 6a4a806..526003b 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -6,4 +6,5 @@ allowBuilds: esbuild: false msgpackr-extract: false prisma: true + sharp: false workerd: false