Initial commit - Horux Despachos NL

This commit is contained in:
2026-05-03 16:47:53 -06:00
commit b00b677c54
647 changed files with 133843 additions and 0 deletions

View File

@@ -0,0 +1,760 @@
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(trial)
databaseName String @unique @map("database_name")
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")
/// Live Secret Key cifrada (AES-256-GCM, misma derivación FIEL_ENCRYPTION_KEY).
/// Cacheada tras primer PUT idempotente a /v2/organizations/{id}/apikeys/live.
facturapiOrgKeyEnc Bytes? @map("facturapi_org_key_enc")
facturapiOrgKeyIv Bytes? @map("facturapi_org_key_iv")
facturapiOrgKeyTag Bytes? @map("facturapi_org_key_tag")
// 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)
// Preferencias de auto-facturación de pagos de suscripción.
// Default: facturar con datos del cliente cuando hay CSF disponible.
// Si `factPreferencia='publico_general'` siempre va a XAXX010101000.
factPreferencia String @default("mis_datos") @map("fact_preferencia") @db.VarChar(20)
// Uso CFDI default cuando se factura con datos del cliente.
// G03 = Gastos en general (más común para SaaS).
factUsoCfdi String @default("G03") @map("fact_uso_cfdi") @db.VarChar(5)
// Si el tenant tiene múltiples regímenes activos, cuál usar para factura.
// Null = usar el primero activo (heurística por createdAt).
factRegimenPreferido String? @map("fact_regimen_preferido") @db.VarChar(3)
// === 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")
// Cuenta sesiones (login exitoso, NO refresh). Usado para auto-dismiss del
// onboarding tras N logins. Default 0 → users pre-rollout siguen viendo el
// onboarding hasta acumular logins post-deploy.
loginCount Int @default(0) @map("login_count")
// Marca explícita de que el onboarding ya no debe mostrarse. Se setea cuando
// el user completa todos los pasos requeridos o desde el endpoint de dismiss.
onboardingDismissedAt DateTime? @map("onboarding_dismissed_at")
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 {
trial
custom
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")
// Idempotencia del cron de aviso pre-vencimiento. Guarda el bucket de días
// que ya se notificó (7, 3, 1 ó 0) para no spamear al owner si el cron corre
// dos veces el mismo día. Se resetea cuando se renueva la suscripción o
// arranca un período nuevo.
lastReminderDay Int? @map("last_reminder_day")
lastReminderSentAt DateTime? @map("last_reminder_sent_at")
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")
}
/// Catálogo despacho — precios + limits editables por admin global.
/// Las `features` siguen viviendo en TS (`DESPACHO_PLANS` en `@horux/shared`)
/// porque están acopladas a UI/middleware y son contrato de código.
/// Incluye filas para `trial` y `custom` (sin precios — null).
model DespachoPlanPrice {
plan String @id // trial | custom | mi_empresa | mi_empresa_plus | business_control | business_cloud
nombre String @db.VarChar(50)
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")
/// Limits del plan. -1 = ilimitado donde aplique (maxUsers).
maxRfcs Int @map("max_rfcs")
maxUsers Int @map("max_users")
timbresIncluidosMes Int @default(0) @map("timbres_incluidos_mes")
dbMode DbMode @map("db_mode")
permiteServidorBackup Boolean @default(false) @map("permite_servidor_backup")
/// Habilita SAT incremental (3 syncs/día adicionales al daily). Mi Empresa +,
/// Business Control y Enterprise lo tienen activo por default; planes
/// inferiores se quedan solo con el daily de las 03:00.
permiteSatIncremental Boolean @default(false) @map("permite_sat_incremental")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("despacho_plan_prices")
}
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)
// Mapa { kindKey: requestId } de TODOS los requests creados durante el job.
// Permite que retries reusen requestIds previos en lugar de quemar cuota
// del SAT creando nuevos. kindKey = `${requestType}-${tipoCfdi}-${from}-${to}`.
satRequestIds Json @default("{}") @map("sat_request_ids")
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")
// True cuando el job es `initial` con rango de fechas personalizado por el
// usuario (botón UI). Cambia la política de retry: 2 intentos vs 3 del
// bootstrap puro. Daily/incremental ignoran este campo.
isCustomRange Boolean @default(false) @map("is_custom_range")
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")
}