Admin fees
SipSop factura fees regulatorios, passthroughs gubernamentales e impuestos de ventas como line items separados en cada factura. Esta página documenta el schema del catálogo de fees, cómo crear y actualizar fees, carrier modes, y cómo los fees fluyen hacia las facturas de Stripe y el componente InvoiceBreakdown.
Fase 16h
Este sistema fue implementado en la Fase 16h del roadmap. La tabla pública de fees se sirve en GET /api/v1/fees/public y se renderiza en /fees en el landing site. El componente de desglose de factura (InvoiceBreakdown) lee los line items desde trpc.billing.invoiceLineItems.
Schema de base de datos
Cuatro tablas en packages/db/src/schema/fees.ts:
fee_definitions
El catálogo maestro de todos los fees. Columnas clave:
| Columna | Tipo | Descripción |
|---|---|---|
id | uuid | PK |
code | text | Identificador único, ej: REGULATORY_RECOVERY, SALES_TAX_NJ. Solo MAYÚSCULAS, letras/números/guión bajo. |
name | text | Nombre visible para clientes |
type | text | Uno de: percentage, flat_per_line, flat_per_invoice, percentage_of_interstate |
rate | numeric(10,8) | Valor de la tasa. Para tipos porcentaje: 0.09 = 9%. Para tipos flat: monto en USD. |
jurisdiction | text | FEDERAL, NJ, NY, NYC, CT, o ALL |
category | text | provider_fee, government_passthrough, o tax |
taxable | boolean | Si este fee es parte de la base imponible de sales tax. Provider fees = true; los impuestos mismos = false. |
compliance_status | text | active, inactive, o configured_pending_registration |
requires_registration | boolean | Si se requiere registro regulatorio (FCC 499, NJ BPU, etc.) antes de activar |
registration_authority | text | FCC, NJ_BPU, NJ_DOT, NY_DTF, etc. |
registration_id | text | El ID de cert/filer real una vez aprobado |
registration_status | text | not_started, in_progress, pending_approval, approved |
effective_from | date | Cuándo empezó a aplicarse esta versión del fee |
effective_to | date | Cuándo dejó de aplicarse esta versión (NULL = vigente) |
supersedes_id | uuid | Apunta a la versión anterior cuando un cambio de rate crea una nueva fila |
Regla de negocio: Nunca hacer hard-delete de fee definitions. Soft-delete via compliance_status='inactive' + effective_to=hoy. Los cambios de rate crean una nueva versión (cierra la fila anterior, inserta una nueva con supersedes_id). Esto preserva el snapshot histórico para cada invoice_line_items.fee_definition_id.
fee_definition_history
Audit log inmutable llenado por un trigger de Postgres en cada INSERT/UPDATE/DELETE a fee_definitions. Nunca escribir en esta tabla desde código de aplicación.
| Columna | Descripción |
|---|---|
operation | INSERT, UPDATE, o DELETE |
previous_values | Snapshot JSONB antes del cambio (NULL para INSERT) |
new_values | Snapshot JSONB después del cambio (NULL para DELETE) |
changed_by_user_id | Seteado via variable de sesión app.current_user_id |
change_reason | Seteado via variable de sesión app.change_reason |
tenant_billing_config
Un row por tenant. Almacena dirección fiscal, código de jurisdicción, carrier mode override, estado de exención impositiva, y exclusiones de fees por cliente.
| Columna | Descripción |
|---|---|
jurisdiction_code | Derivado de billing address: NJ, NY-NYC, NY-WESTCHESTER, etc. |
carrier_mode | reseller_retail, reseller_wholesale, o clec_direct — puede sobrescribir el default del sistema |
tax_exempt | Si es true, no se agregan line items de sales tax |
tax_exempt_certificate | Número de certificado para clientes exentos |
excluded_fee_codes | Array de fee_definitions.code a omitir para este tenant. Para descuentos negociados. NO afecta sales tax (usar tax_exempt para eso). |
invoice_line_items
Un row por línea en una factura generada. Referencia al fee_definition_id que estaba activo en ese momento, preservando las tasas históricas.
| Columna | Descripción |
|---|---|
type | service, addon, usage, fee, o tax |
code | El código del fee (o código del plan para service/addon) |
unit_amount_cents | Cents enteros, nunca floats |
amount_cents | Total de la línea: unit_amount_cents × quantity |
fee_definition_id | FK a fee_definitions. NULL para tipos service, addon, usage. |
jurisdiction | Jurisdicción que disparó esta línea |
billing_system_config
Fila singleton (id = 'singleton'). Tiene el carrier mode global por defecto y los IDs de certs regulatorios (FCC 499 Filer ID, NJ BPU CLEC cert, NJ DoT cert, NY DTF cert).
Tipos de fee y cálculo
percentage → monto = subtotalCents × rate (ej: rate=0.09625 = 9.625%)
flat_per_line → monto = rate × 100 × activeLines (rate en USD → cents)
flat_per_invoice → monto = rate × 100 (monto fijo por factura)
percentage_of_interstate → monto = interstateUsageCents × rateTodos los montos se almacenan y calculan en cents enteros. Sin floats en ningún punto del pipeline de facturación.
Carrier modes
El carrier mode determina qué fees es responsable de cobrar y remitir SipSop:
| Modo | Descripción | Quién maneja passthroughs gubernamentales |
|---|---|---|
reseller_retail | SipSop revende Twilio al retail. Twilio remite E911, FUSF, Cost Recovery. SipSop solo agrega provider fees + sales tax. | Twilio |
reseller_wholesale | SipSop carrier registrado, Twilio wholesale. SipSop cobra y remite TODOS los fees activos. | SipSop |
clec_direct | SipSop como CLEC directo. Máximo control, máxima carga de compliance. | SipSop |
Cambiar carrier mode es una acción exclusiva de super_admin y requiere:
- Para
reseller_wholesale: FCC 499 Filer ID configurado + todos los fees de government passthrough aprobados - Para
clec_direct: FCC 499 Filer ID + Certificación NJ BPU CLEC
La API verifica estos requisitos y bloquea el cambio si no se cumplen. Se requiere un changeReason y la confirmación exacta "I understand this changes regulatory liability".
Procedimientos tRPC (router admin-fees)
Acceso: billingClerkProcedure para CRUD; superAdminProcedure para cambios de carrier mode.
| Procedimiento | Tipo | Descripción |
|---|---|---|
adminFees.list | query | Lista fees con filtros opcionales: jurisdiction, category, complianceStatus, search |
adminFees.get | query | Obtiene un fee por ID |
adminFees.history | query | Historial paginado de fee_definition_history |
adminFees.create | mutation | Crea nueva definición de fee |
adminFees.update | mutation | Actualiza fee. Si el rate cambia, cierra la versión anterior y crea una nueva. |
adminFees.deactivate | mutation | Soft-delete: setea compliance_status='inactive', effective_to=hoy |
adminFees.testCalculation | query | Preview de lo que cobraría un fee dado subtotal/líneas/uso — sin writes en DB |
adminFees.impactAnalysis | query | Cuántas facturas/tenants usaron este fee en los últimos 90 días |
adminFees.compliance | query | Dashboard de compliance: totales por estado + tracker de registro por authority |
adminFees.quarterlyReport | query | Suma de cada fee cobrado en un trimestre (formato: Q1_2026) para filing de remittance |
adminFees.updateRegistration | mutation | Actualiza estado de registro para todos los fees de una authority. También actualiza IDs de cert en billing_system_config. |
adminFees.getCarrierMode | query | Modo actual + checklist de requisitos para upgrade a wholesale/CLEC |
adminFees.setCarrierMode | mutation | Cambiar carrier mode global. Solo super_admin. |
Crear un fee nuevo
- Ir a
/admin/billing/fees/newen el portal. - Campos obligatorios: code (MAYÚSCULAS, ej:
E911_CT), name, type, rate, jurisdiction, category,taxable. - Elegir
compliance_status:inactive— el fee existe en el catálogo pero no aparecerá en facturas.configured_pending_registration— el fee está configurado pero esperando aprobación regulatoria.active— el fee se agregará a las nuevas facturas. Sirequires_registration=true, debe tenerregistration_status='approved'primero (la API lo verifica).
- Proveer un
changeReason(mínimo 10 caracteres) — se almacena enfee_definition_history.
Ejemplo: NJ 911 surcharge
code: E911_NJ
name: NJ 911 Emergency Response Surcharge
type: flat_per_line
rate: 0.90 ← $0.90 por línea activa por mes
jurisdiction: NJ
category: government_passthrough
taxable: false
requires_registration: true
registration_authority: NJ_DOTActualizar un rate
Cuando cambia una tasa gubernamental (ej: el porcentaje de FUSF se actualiza trimestralmente):
- Ir a
/admin/billing/fees/{id}en el portal. - Actualizar el campo
ratey proveerchangeReason. - El servicio cierra la versión actual (
effective_to=hoy) y crea una nueva fila (supersedes_idapunta al ID anterior). Elcodequeda en la nueva fila. - Los
invoice_line_items.fee_definition_idexistentes siguen apuntando a la versión anterior — su tasa histórica se preserva.
Versionado de rate change
No actualizar rate manualmente para períodos pasados. Siempre pasar por la UI/API que crea una nueva versión. Saltear esto rompe la cadena de snapshots históricos usada para reporting de compliance.
Endpoint público de fees
GET /api/v1/fees/public (en apps/api/src/routes/fees-public.ts) devuelve todos los fees active con caché de 5 minutos. Expone solo: code, name, type, rate, jurisdiction, category, taxable. Las notas internas, IDs de registro y metadata nunca se exponen.
Este endpoint alimenta la página pública /fees en el landing site.
Componente de desglose de factura
apps/portal/src/components/billing/invoice-breakdown.tsx renderiza el desglose de line items por factura. Hace:
- Llama a
trpc.billing.invoiceLineItems.useQuery({ invoiceId }). - Agrupa items por tipo: servicios/addons/uso primero, luego fees, luego impuestos.
- Muestra un subtotal después de servicios, un subtotal antes de impuestos, y un total general.
- Renderiza un tooltip (icono
Infode lucide-react) para códigos de fee conocidos usando el mapaFEE_INFOhard-codeado en el componente.
Códigos de fee con tooltips: REGULATORY_RECOVERY, ADMIN_FEE, SALES_TAX_NJ, SALES_TAX_NY_NYC, SALES_TAX_NY_STATE, E911_NJ, FUSF_FEDERAL.
Dashboard de compliance
trpc.adminFees.compliance devuelve:
systemConfig—billing_system_configactual con IDs de certauthorities— agrupado porregistration_authority, con conteos de aprobados/pendientes/no iniciadostotals— conteos total/activos/inactivos/pendingRegistration de fees
Usar esto para verificar que todos los registros requeridos estén en orden antes de cambiar el carrier mode.
Reporte trimestral de compliance
Ejecutar trpc.adminFees.quarterlyReport({ period: 'Q1_2026' }) para obtener el monto total cobrado por cada código de fee durante el trimestre. Usar para:
- Filing trimestral de NJ Sales Tax (formulario ST-50) — suma de items
SALES_TAX_NJ - Remittance de FUSF (si se está en modo wholesale)
- Remittance del surcharge E911 al estado
El reporte agrupa por código de fee e incluye invoiceCount, tenantCount y totalAmountCents.
Audit log
Cada operación de escritura (crear, actualizar, desactivar, cambio de carrier mode, actualización de registro) escribe en audit_log. La tabla es append-only — sin UPDATEs ni DELETEs.
Acciones registradas:
action | Disparador |
|---|---|
fee.create | Nuevo fee creado |
fee.update | Actualización sin cambio de rate |
fee.rate_change | Rate cambiado (nueva versión creada) |
fee.deactivate | Fee soft-deleted |
carrier_mode.change | Carrier mode global cambiado |
registration.update | Estado de registro actualizado para una authority |
Para consultar eventos de auditoría recientes:
SELECT timestamp, action, entity_id, payload, ip_address
FROM audit_log
WHERE entity_type = 'fee_definition'
ORDER BY timestamp DESC
LIMIT 50;