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")
}
