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") } /// Invitaciones de trial enviadas por admin global a tenants específicos. /// Permite activar trials configurables (ej. Business Control Prueba por 60 días) /// con un link único que el owner del tenant puede aceptar. model TrialInvitation { id String @id @default(uuid()) tenantId String @map("tenant_id") invitedBy String @map("invited_by") plan String @default("business_control") durationDays Int @map("duration_days") status String @default("pending") // pending | accepted | expired | cancelled token String @unique emailSentTo String? @map("email_sent_to") sentAt DateTime @default(now()) @map("sent_at") expiresAt DateTime @map("expires_at") acceptedAt DateTime? @map("accepted_at") createdAt DateTime @default(now()) @map("created_at") @@index([tenantId]) @@index([token]) @@index([status]) @@map("trial_invitations") } /// Invitaciones para nuevos clientes enviadas por admin global. /// El destinatario recibe un email con un link para completar su registro. model ClientInvitation { id String @id @default(uuid()) email String invitedBy String @map("invited_by") nombreDespacho String? @map("nombre_despacho") rfc String? status String @default("pending") // pending | accepted | expired token String @unique sentAt DateTime @default(now()) @map("sent_at") expiresAt DateTime @map("expires_at") acceptedAt DateTime? @map("accepted_at") createdAt DateTime @default(now()) @map("created_at") @@index([token]) @@index([status]) @@index([email]) @@map("client_invitations") } /// 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") }