// Horux360 - Database Schema Documentation // PostgreSQL Multi-tenant: database-per-tenant // Última actualización: 2026-04-11 // // ARQUITECTURA: // horux360 (central) ← Prisma: tenants, users, subscriptions, catálogos // horux_ (por tenant) ← Raw SQL (pg Pool): cfdis, cfdi_conceptos, rfcs, alertas generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } // ============================================ // BASE DE DATOS CENTRAL (horux360) // Gestionada por Prisma Client // ============================================ 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") users User[] fielCredential FielCredential? satSyncJobs SatSyncJob[] subscriptions Subscription[] payments Payment[] regimenesIgnorados TenantRegimenIgnorado[] regimenesActivos TenantRegimenActivo[] coeficientes CoeficienteUtilidad[] @@map("tenants") } model User { id String @id @default(uuid()) tenantId String @map("tenant_id") email String @unique passwordHash String @map("password_hash") nombre String role Role @default(visor) active Boolean @default(true) lastLogin DateTime? @map("last_login") createdAt DateTime @default(now()) @map("created_at") tenant Tenant @relation(fields: [tenantId], references: [id]) @@map("users") } 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") } enum Plan { starter business professional enterprise } enum Role { admin contador visor } // ─── Catálogo de Regímenes Fiscales SAT ─── model Regimen { id Int @id @default(autoincrement()) clave String @unique @db.VarChar(3) // Clave SAT: 601, 603, 605, 606, etc. 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") mesRelativo Int @default(1) @map("mes_relativo") mesFijo Int? @map("mes_fijo") recurrencia String @default("mensual") @db.VarChar(20) // mensual, anual usaExtensionRfc Boolean @default(false) @map("usa_extension_rfc") regimenes String @default("todos") // 'todos' o CSV: '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 ─── /// Tasas RESICO (Art. 113-E) 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 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 ─── 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") } // ─── Pagos (MercadoPago) ─── 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") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") tenant Tenant @relation(fields: [tenantId], references: [id]) payments Payment[] @@index([tenantId]) @@index([status]) @@map("subscriptions") } 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") createdAt DateTime @default(now()) @map("created_at") tenant Tenant @relation(fields: [tenantId], references: [id]) subscription Subscription? @relation(fields: [subscriptionId], references: [id]) @@index([tenantId]) @@index([subscriptionId]) @@map("payments") } model SatSyncJob { id String @id @default(uuid()) tenantId String @map("tenant_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 } enum SatSyncStatus { pending running completed failed } enum CfdiSyncType { emitidos recibidos } // ============================================ // BASES DE DATOS POR TENANT (horux_) // Gestionadas por raw SQL via pg Pool // DDL en: apps/api/src/config/database.ts → createTables() // ============================================ // // TABLA: rfcs // id SERIAL PRIMARY KEY // rfc VARCHAR(13) UNIQUE NOT NULL // nombre VARCHAR(255) // creado_en TIMESTAMP DEFAULT NOW() // actualizado_en TIMESTAMP DEFAULT NOW() // // TABLA: cfdis // id SERIAL PRIMARY KEY // year VARCHAR(4), month VARCHAR(2) // type VARCHAR(10) -- EMITIDO | RECIBIDO // uuid VARCHAR(36) UNIQUE // serie, folio VARCHAR(50) // status VARCHAR(20) -- Vigente, Cancelado, 0, 1 // fecha_emision TIMESTAMP // rfc_emisor, rfc_receptor VARCHAR(13) // nombre_emisor, nombre_receptor VARCHAR(255) // subtotal, total, descuento NUMERIC(18,4) (+ _mxn variants) // moneda VARCHAR(3), tipo_cambio NUMERIC(18,6) // tipo_comprobante VARCHAR(1) -- I, E, T, P, N // metodo_pago VARCHAR(3) -- PUE, PPD // forma_pago VARCHAR(2), uso_cfdi VARCHAR(3) // pac VARCHAR(13), fecha_cert_sat, fecha_cancelacion // uuid_relacionado TEXT // -- Impuestos (cada uno con variante _mxn): // isr_retencion, iva_traslado, iva_retencion, ieps_traslado, ieps_retencion // impuestos_locales_trasladado, impuestos_locales_retenidos // -- Complemento de pagos: // monto_pago, fecha_pago_p, num_parcialidad, saldo_pendiente // isr_retencion_pago, iva_traslado_pago, iva_retencion_pago, ieps_traslado_pago, ieps_retencion_pago // -- Nómina: // num_seguro_social, puesto, salario_base_cot_apor, salario_diario_integrado // total_percepciones, total_deducciones, imp_retenidos_nomina, otras_deducciones_nomina, subsidio_causado // -- Metadata: // conciliado VARCHAR(50) // regimen_fiscal_emisor, regimen_fiscal_receptor VARCHAR(3) // rfc_emisor_id, rfc_receptor_id INTEGER REFERENCES rfcs(id) // xml_url, pdf_url, xml_original TEXT // last_sat_sync TIMESTAMP, sat_sync_job_id UUID // source VARCHAR(20) DEFAULT 'manual' // creado_en, actualizado_en TIMESTAMP // // TABLA: cfdi_conceptos // id SERIAL PRIMARY KEY // cfdi_id INTEGER REFERENCES cfdis(id) ON DELETE CASCADE // clave_prod_serv, no_identificacion, descripcion, cantidad, clave_unidad, unidad // valor_unitario, importe, descuento (+ _mxn) // isr_retencion, iva_traslado, iva_retencion, ieps_traslado, ieps_retencion (+ _mxn) // impuestos_locales_trasladado, impuestos_locales_retenidos (+ _mxn) // total_percepciones, total_deducciones, imp_retenidos_nomina, otras_deducciones_nomina, subsidio_causado (+ _mxn) // creado_en TIMESTAMP // // TABLA: alertas // id UUID PRIMARY KEY DEFAULT gen_random_uuid() // tipo VARCHAR(50), titulo VARCHAR(200), mensaje TEXT // prioridad VARCHAR(20) DEFAULT 'media' // fecha_vencimiento TIMESTAMP, leida BOOLEAN, resuelta BOOLEAN // created_at TIMESTAMP // // ÍNDICES: // idx_cfdis_fecha_emision (DESC), idx_cfdis_type, idx_cfdis_status // idx_cfdis_rfc_emisor, idx_cfdis_rfc_receptor // idx_cfdis_year_month, idx_cfdis_rfc_emisor_id, idx_cfdis_rfc_receptor_id // idx_cfdis_nombre_emisor_trgm (GIN trigram), idx_cfdis_nombre_receptor_trgm (GIN trigram) // idx_cfdi_conceptos_cfdi_id, idx_cfdi_conceptos_clave // EXTENSION: pg_trgm (para búsquedas fuzzy por nombre)