Update: nueva version Horux Despachos
This commit is contained in:
744
apps/api/prisma/schema.prisma
Normal file
744
apps/api/prisma/schema.prisma
Normal file
@@ -0,0 +1,744 @@
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model Tenant {
|
||||
id String @id @default(uuid())
|
||||
nombre String
|
||||
rfc String @unique
|
||||
plan Plan @default(starter)
|
||||
databaseName String @unique @map("database_name")
|
||||
cfdiLimit Int @default(100) @map("cfdi_limit")
|
||||
usersLimit Int @default(1) @map("users_limit")
|
||||
active Boolean @default(true)
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
expiresAt DateTime? @map("expires_at")
|
||||
// Prueba gratuita: si está set y en el futuro, el tenant está en trial.
|
||||
// Se consume una sola vez por tenant (al activarla, nunca se regenera).
|
||||
trialEndsAt DateTime? @map("trial_ends_at")
|
||||
|
||||
facturapiOrgId String? @map("facturapi_org_id")
|
||||
|
||||
// Domicilio fiscal
|
||||
codigoPostal String? @map("codigo_postal") @db.VarChar(5)
|
||||
calle String? @db.VarChar(255)
|
||||
numExterior String? @map("num_exterior") @db.VarChar(20)
|
||||
numInterior String? @map("num_interior") @db.VarChar(20)
|
||||
colonia String? @db.VarChar(255)
|
||||
ciudad String? @db.VarChar(100)
|
||||
municipio String? @db.VarChar(100)
|
||||
estado String? @db.VarChar(100)
|
||||
telefono String? @db.VarChar(20)
|
||||
|
||||
// === Despacho fields ===
|
||||
verticalProfile VerticalProfile? @map("vertical_profile")
|
||||
dbMode DbMode? @map("db_mode")
|
||||
dbConnectionEnc String? @map("db_connection_enc")
|
||||
dbConnectionIv String? @map("db_connection_iv")
|
||||
dbSchemaVersion Int @default(0) @map("db_schema_version")
|
||||
connectorTokenEnc String? @map("connector_token_enc")
|
||||
connectorTunnelHostname String? @map("connector_tunnel_hostname")
|
||||
connectorLastSeen DateTime? @map("connector_last_seen")
|
||||
connectorVersion String? @map("connector_version") @db.VarChar(20)
|
||||
|
||||
memberships TenantMembership[]
|
||||
fielCredential FielCredential?
|
||||
satSyncJobs SatSyncJob[]
|
||||
subscriptions Subscription[]
|
||||
payments Payment[]
|
||||
regimenesIgnorados TenantRegimenIgnorado[]
|
||||
regimenesActivos TenantRegimenActivo[]
|
||||
coeficientes CoeficienteUtilidad[]
|
||||
timbreSuscripcion TimbreSuscripcion?
|
||||
timbrePaquetes TimbrePaquete[]
|
||||
connectorHeartbeats ConnectorHeartbeat[]
|
||||
|
||||
@@map("tenants")
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(uuid())
|
||||
email String @unique
|
||||
passwordHash String @map("password_hash")
|
||||
nombre String
|
||||
active Boolean @default(true)
|
||||
lastLogin DateTime? @map("last_login")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
// Contador para invalidar sesiones masivamente. Al incrementar, todos los
|
||||
// JWT emitidos antes (con tokenVersion menor) quedan rechazados en el
|
||||
// siguiente request. Se incrementa en: password change, password reset,
|
||||
// logout-all. Default 0 para compat con users pre-rollout.
|
||||
tokenVersion Int @default(0) @map("token_version")
|
||||
// Último tenant que el user activó (via switch-tenant). Se usa para resolver
|
||||
// el "tenant activo al login". Si es null, el login cae al primer membership
|
||||
// por joinedAt. Se actualiza en cada switch.
|
||||
lastTenantId String? @map("last_tenant_id")
|
||||
|
||||
memberships TenantMembership[]
|
||||
platformRoles UserPlatformRole[]
|
||||
passwordResetTokens PasswordResetToken[]
|
||||
|
||||
@@map("users")
|
||||
}
|
||||
|
||||
/// Relación many-to-many entre User y Tenant. Permite que un mismo user (p.ej.
|
||||
/// un dueño/contador) pertenezca a varios tenants con distintos roles. Esta
|
||||
/// tabla es la fuente de verdad del "¿a qué tenants tiene acceso este user?".
|
||||
///
|
||||
/// Durante la transición, `User.tenantId` y `User.rolId` se mantienen como
|
||||
/// "default tenant" para login UX. El backfill inicial crea 1 membership por
|
||||
/// user basado en esos campos. Cuando se agregue la UI de multi-tenant, los
|
||||
/// nuevos accesos solo tocarán esta tabla.
|
||||
model TenantMembership {
|
||||
id Int @id @default(autoincrement())
|
||||
userId String @map("user_id")
|
||||
tenantId String @map("tenant_id")
|
||||
rolId Int @map("rol_id")
|
||||
isOwner Boolean @default(false) @map("is_owner")
|
||||
active Boolean @default(true)
|
||||
joinedAt DateTime @default(now()) @map("joined_at")
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
||||
rol Rol @relation(fields: [rolId], references: [id])
|
||||
|
||||
@@unique([userId, tenantId])
|
||||
@@index([userId, active])
|
||||
@@index([tenantId, active])
|
||||
@@map("tenant_memberships")
|
||||
}
|
||||
|
||||
model Rol {
|
||||
id Int @id @default(autoincrement())
|
||||
nombre String @unique @db.VarChar(20)
|
||||
descripcion String?
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
memberships TenantMembership[]
|
||||
|
||||
@@map("roles")
|
||||
}
|
||||
|
||||
model RefreshToken {
|
||||
id String @id @default(uuid())
|
||||
userId String @map("user_id")
|
||||
token String @unique
|
||||
expiresAt DateTime @map("expires_at")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
@@map("refresh_tokens")
|
||||
}
|
||||
|
||||
/// Tokens para recuperación de contraseña. Expiran en 1 hora, son single-use
|
||||
/// (se marca `usedAt` al consumir). Al completar reset se invalidan todos los
|
||||
/// refresh tokens del user — cierra todas sus sesiones forzando re-login.
|
||||
model PasswordResetToken {
|
||||
id String @id @default(uuid())
|
||||
userId String @map("user_id")
|
||||
token String @unique
|
||||
expiresAt DateTime @map("expires_at")
|
||||
usedAt DateTime? @map("used_at")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([userId])
|
||||
@@index([expiresAt])
|
||||
@@map("password_reset_tokens")
|
||||
}
|
||||
|
||||
enum Plan {
|
||||
starter
|
||||
business
|
||||
business_ia
|
||||
custom
|
||||
enterprise
|
||||
business_control
|
||||
business_cloud
|
||||
mi_empresa
|
||||
mi_empresa_plus
|
||||
}
|
||||
|
||||
enum VerticalProfile {
|
||||
CONTABLE
|
||||
JURIDICO
|
||||
ARQUITECTURA
|
||||
}
|
||||
|
||||
enum DbMode {
|
||||
BYO
|
||||
MANAGED
|
||||
}
|
||||
|
||||
|
||||
// ============================================
|
||||
// Catálogo de Regímenes Fiscales SAT
|
||||
// ============================================
|
||||
|
||||
model Regimen {
|
||||
id Int @id @default(autoincrement())
|
||||
clave String @unique @db.VarChar(3)
|
||||
descripcion String
|
||||
tipoPersona String @map("tipo_persona") @db.VarChar(20) // fisica, moral, ambos
|
||||
activo Boolean @default(true)
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
tenantIgnorados TenantRegimenIgnorado[]
|
||||
tenantActivos TenantRegimenActivo[]
|
||||
|
||||
@@map("regimenes")
|
||||
}
|
||||
|
||||
model TenantRegimenIgnorado {
|
||||
id Int @id @default(autoincrement())
|
||||
tenantId String @map("tenant_id")
|
||||
regimenId Int @map("regimen_id")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
||||
regimen Regimen @relation(fields: [regimenId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([tenantId, regimenId])
|
||||
@@map("tenant_regimenes_ignorados")
|
||||
}
|
||||
|
||||
model TenantRegimenActivo {
|
||||
id Int @id @default(autoincrement())
|
||||
tenantId String @map("tenant_id")
|
||||
regimenId Int @map("regimen_id")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
||||
regimen Regimen @relation(fields: [regimenId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([tenantId, regimenId])
|
||||
@@map("tenant_regimenes_activos")
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Catálogo de Eventos Fiscales
|
||||
// ============================================
|
||||
|
||||
model EventoFiscalCatalogo {
|
||||
id Int @id @default(autoincrement())
|
||||
titulo String
|
||||
descripcion String?
|
||||
tipo String @db.VarChar(20) // declaracion, pago, obligacion, informativa
|
||||
diaBase Int @map("dia_base") // día del mes (17, 3, 31, etc.)
|
||||
mesRelativo Int @default(1) @map("mes_relativo") // 1=mes posterior, 2=segundo mes posterior, 0=mes fijo
|
||||
mesFijo Int? @map("mes_fijo") // para anuales: 2=feb, 3=mar, 4=abr
|
||||
recurrencia String @default("mensual") @db.VarChar(20) // mensual, anual
|
||||
usaExtensionRfc Boolean @default(false) @map("usa_extension_rfc")
|
||||
regimenes String @default("todos") // 'todos' o CSV de claves: '601,603,612'
|
||||
condicion String? @db.VarChar(50) // null, 'tiene_nomina', 'ingresos_4m'
|
||||
activo Boolean @default(true)
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
@@map("eventos_fiscales_catalogo")
|
||||
}
|
||||
|
||||
/// Lista negra SAT (Art. 69-B CFF)
|
||||
model ListaNegra {
|
||||
id Int @id @default(autoincrement())
|
||||
rfc String @unique @db.VarChar(13)
|
||||
nombre String
|
||||
situacion String @db.VarChar(30) // Definitivo, Presunto, Desvirtuado, Sentencia Favorable
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
@@index([rfc])
|
||||
@@map("lista_negra")
|
||||
}
|
||||
|
||||
/// Días inhábiles fiscales (festivos oficiales de México)
|
||||
model DiaInhabil {
|
||||
id Int @id @default(autoincrement())
|
||||
fecha DateTime @unique @db.Date
|
||||
nombre String
|
||||
|
||||
@@map("dias_inhabiles")
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// ISR Tables
|
||||
// ============================================
|
||||
|
||||
/// Tasas RESICO (Art. 113-E) - tasa plana por bracket mensual
|
||||
model IsrResicoTasa {
|
||||
id Int @id @default(autoincrement())
|
||||
anio Int @map("anio")
|
||||
montoMaximo Decimal @map("monto_maximo") @db.Decimal(18, 2)
|
||||
porcentaje Decimal @db.Decimal(5, 2)
|
||||
|
||||
@@unique([anio, montoMaximo])
|
||||
@@map("isr_resico_tasas")
|
||||
}
|
||||
|
||||
/// Tarifa ISR progresiva (Art. 96) - mensual
|
||||
model IsrTarifa {
|
||||
id Int @id @default(autoincrement())
|
||||
anio Int @map("anio")
|
||||
limiteInferior Decimal @map("limite_inferior") @db.Decimal(18, 2)
|
||||
limiteSuperior Decimal? @map("limite_superior") @db.Decimal(18, 2)
|
||||
cuotaFija Decimal @map("cuota_fija") @db.Decimal(18, 2)
|
||||
porcentajeExcedente Decimal @map("porcentaje_excedente") @db.Decimal(5, 2)
|
||||
|
||||
@@unique([anio, limiteInferior])
|
||||
@@map("isr_tarifas")
|
||||
}
|
||||
|
||||
/// Coeficiente de utilidad por tenant/año (no se sobrescribe)
|
||||
model CoeficienteUtilidad {
|
||||
id Int @id @default(autoincrement())
|
||||
tenantId String @map("tenant_id")
|
||||
anio Int @map("anio")
|
||||
coeficiente Decimal @db.Decimal(10, 4)
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([tenantId, anio])
|
||||
@@map("coeficiente_utilidad")
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// SAT Sync Models
|
||||
// ============================================
|
||||
|
||||
model FielCredential {
|
||||
id String @id @default(uuid())
|
||||
tenantId String @unique @map("tenant_id")
|
||||
rfc String @db.VarChar(13)
|
||||
cerData Bytes @map("cer_data")
|
||||
keyData Bytes @map("key_data")
|
||||
keyPasswordEncrypted Bytes @map("key_password_encrypted")
|
||||
cerIv Bytes @map("cer_iv")
|
||||
cerTag Bytes @map("cer_tag")
|
||||
keyIv Bytes @map("key_iv")
|
||||
keyTag Bytes @map("key_tag")
|
||||
passwordIv Bytes @map("password_iv")
|
||||
passwordTag Bytes @map("password_tag")
|
||||
serialNumber String? @map("serial_number") @db.VarChar(50)
|
||||
validFrom DateTime @map("valid_from")
|
||||
validUntil DateTime @map("valid_until")
|
||||
isActive Boolean @default(true) @map("is_active")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@map("fiel_credentials")
|
||||
}
|
||||
|
||||
model Subscription {
|
||||
id String @id @default(uuid())
|
||||
tenantId String @map("tenant_id")
|
||||
plan Plan
|
||||
mpPreapprovalId String? @map("mp_preapproval_id")
|
||||
status String @default("pending")
|
||||
amount Decimal @db.Decimal(10, 2)
|
||||
frequency String @default("monthly")
|
||||
currentPeriodStart DateTime? @map("current_period_start")
|
||||
currentPeriodEnd DateTime? @map("current_period_end")
|
||||
// Cambio programado al próximo período (downgrades y cambios de frecuencia)
|
||||
pendingPlan Plan? @map("pending_plan")
|
||||
pendingFrequency String? @map("pending_frequency")
|
||||
pendingEffectiveAt DateTime? @map("pending_effective_at")
|
||||
// Upgrade inmediato en curso: preference MP esperando cobro prorateado.
|
||||
// Cuando el webhook confirma el pago, se aplica el plan nuevo y se limpian estos campos.
|
||||
upgradePreferenceId String? @map("upgrade_preference_id")
|
||||
upgradeTargetPlan Plan? @map("upgrade_target_plan")
|
||||
upgradeTargetAmount Decimal? @db.Decimal(10, 2) @map("upgrade_target_amount")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
tenant Tenant @relation(fields: [tenantId], references: [id])
|
||||
payments Payment[]
|
||||
addons SubscriptionAddon[]
|
||||
|
||||
@@index([tenantId])
|
||||
@@index([status])
|
||||
@@index([pendingEffectiveAt])
|
||||
@@map("subscriptions")
|
||||
}
|
||||
|
||||
model SubscriptionAddon {
|
||||
id String @id @default(uuid())
|
||||
subscriptionId String @map("subscription_id")
|
||||
planAddonCatalogoId String @map("plan_addon_catalogo_id")
|
||||
/// UUID del contribuyente (entidad_id en tenant BD) cuando el add-on
|
||||
/// aplica a un RFC específico. NULL para add-ons a nivel tenant (módulos
|
||||
/// globales, +RFCs, +timbres). Sin FK porque contribuyente vive en BD tenant.
|
||||
contribuyenteId String? @map("contribuyente_id")
|
||||
mpPreapprovalId String? @map("mp_preapproval_id")
|
||||
status String @default("pending")
|
||||
quantity Int @default(1)
|
||||
amount Decimal @db.Decimal(10, 2)
|
||||
currentPeriodStart DateTime? @map("current_period_start")
|
||||
currentPeriodEnd DateTime? @map("current_period_end")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
subscription Subscription @relation(fields: [subscriptionId], references: [id])
|
||||
planAddonCatalogo PlanAddonCatalogo @relation(fields: [planAddonCatalogoId], references: [id])
|
||||
|
||||
/// Sin UNIQUE compuesto: la validación de "un solo add-on activo por
|
||||
/// (subscription, addon, contribuyente?)" queda a nivel aplicación
|
||||
/// (findFirst en subscribeAddon), porque Postgres trata NULL!=NULL y no
|
||||
/// hay forma trivial de enforcar unicidad con contribuyenteId opcional.
|
||||
@@index([subscriptionId])
|
||||
@@index([subscriptionId, contribuyenteId])
|
||||
@@map("subscription_addons")
|
||||
}
|
||||
|
||||
/// Roles de plataforma (staff interno de Horux 360) — ortogonales al rol per-tenant.
|
||||
/// Un user puede tener 0, 1 o varios roles. `platform_admin` es el superrol.
|
||||
/// Ver `docs/plans/2026-04-14-platform-admin-roles.md`.
|
||||
enum PlatformRole {
|
||||
platform_admin // Todo: precios, clientes, facturas, suscripciones, gestión de staff
|
||||
platform_ti // Mismos permisos que admin (equipo de TI / tech ops). Diferencia solo en trazabilidad.
|
||||
platform_support // Ver todos los tenants, resolver tickets, NO facturación/precios
|
||||
platform_sales // Crear/editar tenants (onboarding), ver suscripciones, NO precios
|
||||
platform_finance // Ver payments, emitir facturas manuales, editar precios, reportes fiscales
|
||||
}
|
||||
|
||||
model UserPlatformRole {
|
||||
id Int @id @default(autoincrement())
|
||||
userId String @map("user_id")
|
||||
role PlatformRole
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
createdBy String? @map("created_by") // User.id de quien asignó (audit trail)
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([userId, role])
|
||||
@@index([role])
|
||||
@@map("user_platform_roles")
|
||||
}
|
||||
|
||||
/// Registro de acciones críticas para auditoría (SAT compliance, forense, disputas).
|
||||
/// Se instrumenta vía `utils/audit.ts` con helper fire-and-forget — un fallo al
|
||||
/// escribir aquí NUNCA debe romper la acción principal.
|
||||
model AuditLog {
|
||||
id String @id @default(uuid())
|
||||
userId String? @map("user_id")
|
||||
tenantId String? @map("tenant_id")
|
||||
action String @db.VarChar(64) // "price.updated", "subscription.cancelled", etc.
|
||||
entityType String? @map("entity_type") @db.VarChar(32)
|
||||
entityId String? @map("entity_id")
|
||||
metadata Json? // before/after, ip, userAgent, contexto
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
@@index([userId, createdAt])
|
||||
@@index([tenantId, createdAt])
|
||||
@@index([action, createdAt])
|
||||
@@index([entityType, entityId])
|
||||
@@map("audit_log")
|
||||
}
|
||||
|
||||
/// Padrón persistente de RFCs que ya consumieron su prueba gratuita de 30 días.
|
||||
/// Sobrevive al ciclo de vida del Tenant (si se borra/recrea, el RFC sigue aquí),
|
||||
/// bloqueando el abuso de "registro nuevo con el mismo RFC para otro trial".
|
||||
model TrialUsage {
|
||||
id Int @id @default(autoincrement())
|
||||
rfc String @unique @db.VarChar(13)
|
||||
tenantId String? @map("tenant_id") // Tenant que consumió (null si el tenant se borró después)
|
||||
startedAt DateTime @default(now()) @map("started_at")
|
||||
|
||||
@@map("trial_usages")
|
||||
}
|
||||
|
||||
/// Precios editables de los planes (self-serve). Custom no se guarda aquí
|
||||
/// porque cada cliente tiene su monto propio (lo fija el admin al crear tenant).
|
||||
model PlanPrice {
|
||||
id Int @id @default(autoincrement())
|
||||
plan Plan
|
||||
frequency String // "monthly" | "annual"
|
||||
amount Decimal @db.Decimal(10, 2)
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
@@unique([plan, frequency])
|
||||
@@map("plan_prices")
|
||||
}
|
||||
|
||||
/// Precios editables por admin global de los planes despacho.
|
||||
/// Antes vivía en `DESPACHO_PLAN_PRICES` (catálogo estático en `@horux/shared`);
|
||||
/// movido a BD para permitir actualización desde `/configuracion/precios-suscripcion`.
|
||||
/// Si una fila no existe, `getPlanPrice` cae al catálogo estático como fallback.
|
||||
model DespachoPlanPrice {
|
||||
plan String @id // mi_empresa | mi_empresa_plus | business_control | business_cloud
|
||||
monthly Decimal? @db.Decimal(10, 2)
|
||||
firstYear Decimal @db.Decimal(10, 2) @map("first_year")
|
||||
renewal Decimal @db.Decimal(10, 2)
|
||||
permiteMonthly Boolean @default(false) @map("permite_monthly")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
@@map("despacho_plan_prices")
|
||||
}
|
||||
|
||||
model PlanCatalogo {
|
||||
id String @id @default(uuid())
|
||||
codename String @unique @db.VarChar(50)
|
||||
nombre String
|
||||
verticalProfile VerticalProfile
|
||||
precioBase Decimal @db.Decimal(10, 2) @map("precio_base")
|
||||
frecuencia String @db.VarChar(10)
|
||||
limits Json
|
||||
active Boolean @default(true)
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
@@map("plan_catalogo")
|
||||
}
|
||||
|
||||
model PlanAddonCatalogo {
|
||||
id String @id @default(uuid())
|
||||
codename String @unique @db.VarChar(50)
|
||||
nombre String
|
||||
verticalProfile VerticalProfile?
|
||||
precio Decimal @db.Decimal(10, 2)
|
||||
frecuencia String @db.VarChar(10)
|
||||
delta Json
|
||||
active Boolean @default(true)
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
subscriptionAddons SubscriptionAddon[]
|
||||
|
||||
@@map("plan_addon_catalogo")
|
||||
}
|
||||
|
||||
model ConnectorHeartbeat {
|
||||
id String @id @default(uuid())
|
||||
tenantId String @map("tenant_id")
|
||||
timestamp DateTime @default(now())
|
||||
latencyMs Int @map("latency_ms")
|
||||
version String @db.VarChar(20)
|
||||
pgVersion String? @map("pg_version") @db.VarChar(50)
|
||||
status String @db.VarChar(20)
|
||||
errorMsg String? @map("error_msg")
|
||||
|
||||
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([tenantId, timestamp])
|
||||
@@map("connector_heartbeats")
|
||||
}
|
||||
|
||||
enum PaymentKind {
|
||||
subscription
|
||||
timbres_pack
|
||||
}
|
||||
|
||||
model Payment {
|
||||
id String @id @default(uuid())
|
||||
tenantId String @map("tenant_id")
|
||||
subscriptionId String? @map("subscription_id")
|
||||
mpPaymentId String? @map("mp_payment_id")
|
||||
amount Decimal @db.Decimal(10, 2)
|
||||
status String @default("pending")
|
||||
paymentMethod String? @map("payment_method")
|
||||
paidAt DateTime? @map("paid_at")
|
||||
// Tipo de pago. subscription = cobro mensual/anual del plan.
|
||||
// timbres_pack = compra de paquete de timbres adicionales.
|
||||
kind PaymentKind @default(subscription)
|
||||
// ID de la factura emitida auto por Facturapi. Null si no se facturó:
|
||||
// primer pago (manual), trial sin monto, o fallo al emitir.
|
||||
facturapiInvoiceId String? @map("facturapi_invoice_id")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
tenant Tenant @relation(fields: [tenantId], references: [id])
|
||||
subscription Subscription? @relation(fields: [subscriptionId], references: [id])
|
||||
timbrePaquete TimbrePaquete?
|
||||
|
||||
@@index([tenantId])
|
||||
@@index([subscriptionId])
|
||||
@@map("payments")
|
||||
}
|
||||
|
||||
/// Catálogo de paquetes de timbres adicionales vendibles. Precios editables
|
||||
/// desde panel admin. Los 3 defaults (100/$200, 1000/$1400, 10000/$8600) se
|
||||
/// insertan en seed idempotente.
|
||||
model TimbrePaqueteCatalogo {
|
||||
id Int @id @default(autoincrement())
|
||||
cantidad Int @unique // 100, 1000, 10000
|
||||
precio Decimal @db.Decimal(10, 2)
|
||||
active Boolean @default(true)
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
@@map("timbre_paquetes_catalogo")
|
||||
}
|
||||
|
||||
/// Compra individual de timbres adicionales. Los timbres del plan (mensuales)
|
||||
/// se rastrean en TimbreSuscripcion — esto es SOLO para los extras pagados.
|
||||
/// Vigencia 1 año desde `adquiridoEn`. El orden de consumo es FIFO por
|
||||
/// `expiraEn` (menor primero) para no desperdiciar paquetes próximos a vencer.
|
||||
model TimbrePaquete {
|
||||
id Int @id @default(autoincrement())
|
||||
tenantId String @map("tenant_id")
|
||||
paymentId String? @unique @map("payment_id") // Payment que lo compró; null si admin grant manual
|
||||
cantidad Int // cuántos timbres tenía originalmente
|
||||
usados Int @default(0)
|
||||
precio Decimal @db.Decimal(10, 2) // precio pagado (historial, no cambia si el catálogo cambia)
|
||||
adquiridoEn DateTime @default(now()) @map("adquirido_en")
|
||||
expiraEn DateTime @map("expira_en") // adquiridoEn + 1 año
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
||||
payment Payment? @relation(fields: [paymentId], references: [id])
|
||||
|
||||
@@index([tenantId, expiraEn])
|
||||
@@map("timbre_paquetes")
|
||||
}
|
||||
|
||||
model SatSyncJob {
|
||||
id String @id @default(uuid())
|
||||
tenantId String @map("tenant_id")
|
||||
contribuyenteId String? @map("contribuyente_id")
|
||||
type SatSyncType
|
||||
status SatSyncStatus @default(pending)
|
||||
dateFrom DateTime @map("date_from") @db.Date
|
||||
dateTo DateTime @map("date_to") @db.Date
|
||||
cfdiType CfdiSyncType? @map("cfdi_type")
|
||||
satRequestId String? @map("sat_request_id") @db.VarChar(50)
|
||||
satPackageIds String[] @map("sat_package_ids")
|
||||
cfdisFound Int @default(0) @map("cfdis_found")
|
||||
cfdisDownloaded Int @default(0) @map("cfdis_downloaded")
|
||||
cfdisInserted Int @default(0) @map("cfdis_inserted")
|
||||
cfdisUpdated Int @default(0) @map("cfdis_updated")
|
||||
progressPercent Int @default(0) @map("progress_percent")
|
||||
errorMessage String? @map("error_message")
|
||||
startedAt DateTime? @map("started_at")
|
||||
completedAt DateTime? @map("completed_at")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
retryCount Int @default(0) @map("retry_count")
|
||||
nextRetryAt DateTime? @map("next_retry_at")
|
||||
|
||||
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([tenantId])
|
||||
@@index([status])
|
||||
@@index([status, nextRetryAt])
|
||||
@@map("sat_sync_jobs")
|
||||
}
|
||||
|
||||
enum SatSyncType {
|
||||
initial
|
||||
daily
|
||||
incremental
|
||||
}
|
||||
|
||||
enum SatSyncStatus {
|
||||
pending
|
||||
running
|
||||
completed
|
||||
failed
|
||||
}
|
||||
|
||||
enum CfdiSyncType {
|
||||
emitidos
|
||||
recibidos
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Catálogos SAT para Facturación (CFDI 4.0)
|
||||
// ============================================
|
||||
|
||||
model CatFormaPago {
|
||||
id Int @id @default(autoincrement())
|
||||
clave String @unique @db.VarChar(2)
|
||||
descripcion String
|
||||
|
||||
@@map("cat_forma_pago")
|
||||
}
|
||||
|
||||
model CatMetodoPago {
|
||||
id Int @id @default(autoincrement())
|
||||
clave String @unique @db.VarChar(3)
|
||||
descripcion String
|
||||
|
||||
@@map("cat_metodo_pago")
|
||||
}
|
||||
|
||||
model CatUsoCfdi {
|
||||
id Int @id @default(autoincrement())
|
||||
clave String @unique @db.VarChar(4)
|
||||
descripcion String
|
||||
personaFisica Boolean @default(true) @map("persona_fisica")
|
||||
personaMoral Boolean @default(true) @map("persona_moral")
|
||||
|
||||
@@map("cat_uso_cfdi")
|
||||
}
|
||||
|
||||
model CatMoneda {
|
||||
id Int @id @default(autoincrement())
|
||||
clave String @unique @db.VarChar(3)
|
||||
descripcion String
|
||||
decimales Int @default(2)
|
||||
|
||||
@@map("cat_moneda")
|
||||
}
|
||||
|
||||
model CatClaveUnidad {
|
||||
id Int @id @default(autoincrement())
|
||||
clave String @unique @db.VarChar(10)
|
||||
descripcion String
|
||||
|
||||
@@map("cat_clave_unidad")
|
||||
}
|
||||
|
||||
model CatClaveProdServ {
|
||||
id Int @id @default(autoincrement())
|
||||
clave String @unique @db.VarChar(8)
|
||||
descripcion String
|
||||
|
||||
@@index([descripcion])
|
||||
@@map("cat_clave_prod_serv")
|
||||
}
|
||||
|
||||
model CatObjetoImp {
|
||||
id Int @id @default(autoincrement())
|
||||
clave String @unique @db.VarChar(2)
|
||||
descripcion String
|
||||
|
||||
@@map("cat_objeto_imp")
|
||||
}
|
||||
|
||||
model CatTipoRelacion {
|
||||
id Int @id @default(autoincrement())
|
||||
clave String @unique @db.VarChar(2)
|
||||
descripcion String
|
||||
|
||||
@@map("cat_tipo_relacion")
|
||||
}
|
||||
|
||||
model CatExportacion {
|
||||
id Int @id @default(autoincrement())
|
||||
clave String @unique @db.VarChar(2)
|
||||
descripcion String
|
||||
|
||||
@@map("cat_exportacion")
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Gestión de Timbres Facturapi
|
||||
// ============================================
|
||||
|
||||
model TimbreSuscripcion {
|
||||
id Int @id @default(autoincrement())
|
||||
tenantId String @unique @map("tenant_id")
|
||||
tipo String @db.VarChar(10) // mensual, anual
|
||||
timbresLimite Int @map("timbres_limite") // 50 o 600
|
||||
timbresUsados Int @default(0) @map("timbres_usados")
|
||||
periodoInicio DateTime @map("periodo_inicio") @db.Date
|
||||
periodoFin DateTime @map("periodo_fin") @db.Date
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@map("timbre_suscripciones")
|
||||
}
|
||||
Reference in New Issue
Block a user