🏗️ Terry 3.0

Especificación técnica · v1.0 · 2026-11-05 · Innova IA Soft

🎯 Objetivo

Plataforma de gestión inmobiliaria: administradores registran desarrollos (proyectos), generan enlaces de invitación para asesores, los asesores promocionan proyectos mediante códigos propios para inversionistas, y los inversionistas apartan lotes con validación por OTP. Incluye conciliación básica (cambio de estatus de lote).

Stack: Python 3.13 + FastAPI + PostgreSQL 18 + JWT + React (Vite + Tailwind).
Reconstruye Terry 1/2 (.NET 9 + Blazor + SQL Server) sobre stack validable en este servidor.

🚫 Fuera de alcance (v1)

👤 Roles y permisos

Regla de oro: los permisos se cuelgan de la membresía (usuario × proyecto), no del usuario global. Un mismo usuario puede ser Admin de sus proyectos y Asesor en proyectos ajenos.

Roles por proyecto

RolCómo se obtiene
👑 AdminSe registra en la plataforma y crea un proyecto
🤝 AsesorEntra por enlace de invitación del proyecto (código de proyecto)
🧾 ConciliadorAsesor con permisos extra; el Admin lo asigna dentro del equipo del proyecto
💼 InversionistaEntra con email + código de asesor + OTP. Sin cuenta ni contraseña

Registro unificado

Una sola cuenta. Lo que determina los permisos es la acción:

AcciónResultado
Crear un proyectoEres Admin de ese proyecto
Aceptar invitación (código de proyecto)Eres Asesor de ese proyecto
El Admin del proyecto te asigna conciliadorEres Conciliador en ese proyecto
Apartar un loteEres Comprador (cualquier rol puede comprar)
🔮 Futuro: El Admin será quien pague. Un Asesor que quiera crear proyectos deberá escalar su plan.

Matriz de permisos

Capacidad👑 Admin🤝 Asesor🧾 Conciliador💼 Inversionista
Crear / editar / habilitar proyecto✅———
Generar código de proyecto (para asesores)✅———
Asignar conciliador en su proyecto✅———
Ver equipo del proyecto✅———
Ver proyecto, etapas y lotes✅✅✅✅
Generar código de asesor (para inversionistas)✅✅✅—
Apartar lote para sí mismo✅✅✅✅
Apartar lote a nombre de tercero✅✅✅—
Cambiar estatus de lote✅—✅—

🔗 Códigos de invitación

TipoGenerado porParaPersistencia
🔵 Código de proyecto Admin (al crear proyecto) Asesores se registren como colaboradores Fijo, nunca cambia
🟢 Código de asesor Asesor/Admin (desde dashboard) Inversionistas accedan al proyecto y aparten No expira — asigna comisión al asesor

Enlaces de ejemplo

TipoURL de ejemplo
Para asesoresterry3.pages.dev/unirse?codigo=proj_a7b3x9k2
Para inversionistasterry3.pages.dev/comprar?inv=inv_xyz_asesor1

🧠 Modelo de datos

── Usuarios ──
usuarios
  id               UUID  PK
  email            TEXT  UNIQUE NOT NULL
  email_validado   BOOL  DEFAULT false       ← inversionistas validan con OTP
  nombre           TEXT  NOT NULL
  password_hash    TEXT  NULL                ← internos (Admin/Asesor); inversionista NULL
  created_at       TIMESTAMPTZ DEFAULT now()

── Proyectos ──
proyectos
  id               UUID  PK
  admin_id         UUID  FK usuarios.id      ← dueño
  nombre           TEXT  NOT NULL
  descripcion      TEXT
  ubicacion        TEXT
  estatus          TEXT  CHECK (estatus IN ('activo','inactivo','cerrado'))
  codigo_invitacion TEXT UNIQUE NOT NULL      ← fijo, NUNCA cambia (para asesores)

── Membresías (roles por proyecto) ──
membresias
  id               UUID  PK
  proyecto_id      UUID  FK proyectos.id
  usuario_id       UUID  FK usuarios.id
  rol              TEXT  CHECK (rol IN ('asesor','conciliador'))
  activo           BOOL  DEFAULT true         ← asesor lo pone false al "dejar de promover"

── Códigos de asesor (promoción) ──
codigos_asesor
  id               UUID  PK
  proyecto_id      UUID  FK proyectos.id
  asesor_id        UUID  FK usuarios.id       ← quien promociona (Admin también)
  codigo           TEXT  UNIQUE NOT NULL       ← NO expira — permanente

── Etapas ──
etapas
  id               UUID  PK
  proyecto_id      UUID  FK proyectos.id
  nombre           TEXT  NOT NULL
  fecha_entrega    DATE
  orden            INT
  en_venta         BOOL  DEFAULT false
  tiempo_apartado  INT  DEFAULT 72            ← horas para subir comprobante

── Lotes ──
lotes
  id               UUID  PK
  etapa_id         UUID  FK etapas.id
  numero           TEXT  NOT NULL
  m2               NUMERIC(10,2)
  precio           NUMERIC(12,2) NOT NULL
  moneda           TEXT  DEFAULT 'MXN'
  estatus          TEXT  CHECK (estatus IN ('disponible','apartado','regular','mora','pagado'))

── Apartados ──
apartados
  id               UUID  PK
  lote_id          UUID  FK lotes.id  UNIQUE
  inversionista_id UUID  FK usuarios.id     ← comprador (siempre existe y validado)
  apartado_por     UUID  FK usuarios.id NULL ← quien apartó a nombre de otro
  codigo_asesor    TEXT  FK codigos_asesor   ← para asignar comisión
  estatus          TEXT  DEFAULT 'apartado'
  expira_en        TIMESTAMPTZ              ← now + etapa.tiempo_apartado
  comprobante_url  TEXT  NULL
  solicita_cancelacion BOOL DEFAULT false
  cancel_aprobado_por UUID FK usuarios.id NULL
  cancel_ejecutado_por UUID FK usuarios.id NULL

📊 Estados

Proyecto

🟢 Activo 🟡 Inactivo 🔴 Cerrado
EstadoComportamiento
ActivoVisible para asesores e inversionistas. Se puede apartar.
InactivoMuestra "Proyecto en pausa, pronto estará disponible." No se puede apartar. Asesores existentes siguen viéndolo.
CerradoMuestra "Proyecto vendido en su totalidad. Gracias." Sin operaciones.

Lote

disponible ──→ apartado ──→ regular ──→ pagado │ │ ↓ ↓ (vence sin (mora) comprobante) ↓ disponible

📡 Endpoints

MétodoRutaAuthDescripción
POST/api/auth/registro—Registrar cuenta (cualquier usuario)
POST/api/auth/login—Login email + contraseña
POST/api/auth/refreshrefreshRenovar JWT
POST/api/auth/invitacion-proyecto—Aceptar código de proyecto: crea membresía asesor
POST/api/auth/invitacion-inversionista—Inicio con código de asesor
POST/api/auth/otp/enviar—Enviar OTP al email del inversionista
POST/api/auth/otp/verificar—Validar OTP → email_validado = true
GET/api/auth/check-acceso—Verificar si email ya fue validado (login sin OTP)
GET/api/proyectosJWTMis proyectos (como admin y como miembro)
POST/api/proyectosJWTCrear proyecto
PATCH/api/proyectos/{id}JWT adminEditar / cambiar estatus
GET/api/proyectos/{id}JWTDetalle del proyecto
GET/api/proyectos/{id}/etapasJWTEtapas del proyecto
PATCH/api/proyectos/{id}/etapa-activaJWT adminCambiar etapa en venta
GET/api/proyectos/{id}/lotesJWTLotes con filtros (etapa, estatus)
POST/api/proyectos/{id}/codigo-asesorJWTGenerar código de asesor para inversionistas
GET/api/proyectos/{id}/equipoJWT adminVer colaboradores
PATCH/api/proyectos/{id}/equipo/{usuario}JWT adminAsignar/quitar conciliador
POST/api/lotes/apartarJWTApartar uno o varios lotes (body: lotes[], codigo_asesor)
POST/api/lotes/{id}/comprobanteJWTSubir comprobante de pago
PATCH/api/lotes/{id}/estatusJWT conciliador/adminCambiar estatus
POST/api/lotes/{id}/solicitar-cancelacionJWTInversionista solicita cancelación
POST/api/lotes/{id}/aprobar-cancelacionJWT asesorAsesor da visto bueno
POST/api/lotes/{id}/ejecutar-cancelacionJWT conciliadorConciliador ejecuta cancelación
POST/api/lotes/liberar-expiradosJWT adminLiberar lotes cuyo apartado venció (cron)

🔄 Flujos clave

1. Alta de Asesor

Admin crea proyecto → genera código de proyecto (fijo) Admin comparte enlace: terry3.pages.dev/unirse?codigo=proj_a7b3x Asesor abre el enlace: ┝ ¿No tiene cuenta? → formulario: nombre + email + contraseña ┝ ¿Tiene cuenta? → login └ Tras autenticar → membresía(proyecto, usuario, asesor) creada Si ya es miembro → redirige al dashboard (sin error)

2. Flujo del Inversionista

Inversionista recibe enlace: terry3.pages.dev/comprar?inv=inv_xyz ┝ ¿Primera vez? │ → Ingresa su email │ → Recibe OTP → lo valida │ → email_validado = true │ → Se crea usuario (sin contraseña) │ → Ingresa al proyecto └ ¿Ya validado antes? → Ingresa su email → Sistema detecta email_validado = true → Acceso directo (sin OTP) Dentro: → Ve etapa activa del proyecto → Cambia entre etapas abiertas (mismo proyecto) → NO puede ver otro proyecto Selección: → Selecciona/deselecciona lotes libremente → Panel muestra resumen con cuenta total Confirmación: → API revalida disponibilidad de CADA lote (FOR UPDATE) → ¿Algún lote ya no disponible? → notifica, ajusta → ¿Todos disponibles? → apartado creado

3. Apartado con expiración

POST /lotes/apartar → Transacción FOR UPDATE sobre cada lote → Si algún lote no disponible → 409 (se notifica cuáles) → Crea apartados con expira_en = now() + etapa.tiempo_apartado → Lotes pasan a 'apartado' El inversionista tiene hasta expira_en para subir comprobante: POST /lotes/{id}/comprobante El conciliador/admin aprueba: PATCH /lotes/{id}/estatus → 'regular' Si expira sin comprobante: endpoint/lotes/liberar-expirados → lote vuelve a 'disponible'

4. Cancelación

Inversionista → POST /solicitar-cancelacion → solicita_cancelacion = true Asesor → POST /aprobar-cancelacion → cancel_aprobado_por = asesor.id Conciliador → POST /ejecutar-cancelacion → lote.estatus = 'disponible' → apartado cerrado

5. Conciliador por proyecto

Admin → GET equipo → ve lista de asesores → PATCH /equipo/{usuario} → marca como 'conciliador' El mismo usuario en OTRO proyecto sigue siendo 'asesor'

6. Desvinculación de asesor

Asesor desde su vista del proyecto: "Dejar de promover" → membresia.activo = false Admin ya no lo ve en su equipo de ese proyecto La desvinculación es UNILATERAL (la decide el asesor)

🔐 Seguridad

🌐 Dominio y Deploy

RecursoURL
Spec visualterry3-spec.pages.dev ← estás aquí
APIDokploy — dominio por asignar (sslip.io)
Frontendterry3.pages.dev (pendiente)
Base de datosPostgreSQL 18 en Dokploy — base terry3

Deploy

✅ Decisiones cerradas

FechaDecisiónMotivo
2026-11-05Stack: FastAPI + PostgreSQL 18 + JWT + AlembicValidable aquí (no hay .NET)
2026-11-05Sin capa Organización; cada cuenta Admin es su propio tenantSimplificación
2026-11-05Permisos por membresía (usuario × proyecto), no rol globalAdmin=asesor de otro, conciliador solo en un proyecto
2026-11-05Registro unificado — permisos los da la acción, no el formularioResuelve asesor→admin naturalmente
2026-11-05Todos pueden comprar; internos apartan para tercerosRequisito de Jorge
2026-11-05Dos tipos de código: proyecto (asesores, fijo) y asesor (inversionistas)Flujo de negocio real
2026-11-05Inversionista: email + OTP (1ª vez) → solo email (siguientes)Mínima fricción
2026-11-05Código de proyecto: fijo, nunca cambiaNo romper asociaciones
2026-11-05Código de asesor: no expira, permanenteAsigna comisión al asesor
2026-11-05Admin también genera código de asesorConsistente: todos promocionan
2026-11-05Desvinculación: la decide el asesor, no el adminAutogestión
2026-11-05Solo se aparta para cuentas verificadasEvita apartados fantasmas
2026-11-05Cancelación: solicitud → asesor → conciliadorFlujo con supervisión
2026-11-05Selección múltiple de lotes + carrito + revalidaciónComo en Terry actual
2026-11-05Conciliador v1 = asesor + cambio de estatus (sin pagos)Alcance acotado
2026-11-05Estados de proyecto: activo / inactivo / cerradoControl del admin
2026-11-05Apartados con expiración por etapa + comprobanteFlujo real de compra-venta
2026-11-05Frontend React + Vite + Tailwind; API separadaDecisión de Jorge
2026-11-05Empezar de cero, sin migrar—

📋 Pendientes para v2

🧭 Próximos pasos

  1. ✅ Spec v1.0 aprobado (estás aquí)
  2. ⬜ Crear repositorio terry3 en GitHub
  3. ⬜ Crear base terry3 en PostgreSQL 18 (Dokploy)
  4. ⬜ API + Alembic + migraciones
  5. ⬜ Lógica de auth (JWT + OTP + códigos de invitación)
  6. ⬜ CRUD de proyectos, etapas, lotes
  7. ⬜ Flujo de apartado con concurrencia
  8. ⬜ Frontend React
  9. ⬜ Registrar en _REFERENCIA.md y tarjeta en el taller
  10. ⬜ Despliegue completo