🏗️ 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               INT   PK IDENTITY(1,1)
  email            TEXT  UNIQUE NOT NULL
  email_validado   BOOL  DEFAULT false       ← TODOS validan con OTP
  nombre           TEXT  NOT NULL
  password_hash    TEXT  NULL                ← internos; inversionista NULL
  created_at       TIMESTAMPTZ DEFAULT now()

── Proyectos ──
proyectos
  id               INT   PK IDENTITY(1,1)
  admin_id         INT   FK usuarios.id
  nombre           TEXT  NOT NULL
  descripcion      TEXT
  ubicacion        TEXT
  estatus          TEXT  CHECK (estatus IN ('activo','inactivo','cerrado'))
  codigo_invitacion TEXT UNIQUE NOT NULL      ← fijo, para asesores
  imagen_representativa TEXT NULL
  logo_url          TEXT NULL
  imagen_notificacion TEXT NULL

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

── Códigos de asesor (promoción) ──
codigos_asesor
  id               INT   PK IDENTITY(1,1)
  proyecto_id      INT   FK proyectos.id
  asesor_id        INT   FK usuarios.id
  codigo           TEXT  UNIQUE NOT NULL       ← NO expira, asigna comisión

── Etapas ──
etapas
  id               INT   PK IDENTITY(1,1)
  proyecto_id      INT   FK proyectos.id
  nombre           TEXT  NOT NULL
  fecha_entrega    DATE
  orden            INT
  en_venta         BOOL  DEFAULT false
  estatus          TEXT  DEFAULT 'abierta'     ← abierta / cerrada
  tiempo_apartado  INT   DEFAULT 72            ← horas para subir comprobante
  imagen_lotificacion TEXT NULL
  imagen_cuenta     TEXT NULL
  banco             TEXT NULL
  numero_cuenta     TEXT NULL
  beneficiario      TEXT NULL
  concepto_pago     TEXT NULL
  ── Solo modalidad 'tickets' ──
  total_tickets     INT   NULL                 ← cuántos tickets
  precio_ticket     NUMERIC(12,2) NULL         ← precio unitario
  created_at       TIMESTAMPTZ DEFAULT now()

── Promociones (descuentos por unidad) ──
promociones
  id               INT   PK IDENTITY(1,1)
  etapa_id         INT   FK etapas.id
  cantidad_min     INT   NOT NULL             ← mínimo de unidades para la tarifa (ej. 1, 3)
  descuento_unidad NUMERIC(12,2) NOT NULL     ← descuento POR UNIDAD (ej. 10000, 15000)

── Lotes ──
lotes
  id               INT   PK IDENTITY(1,1)
  etapa_id         INT   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'))
  coordenadas_mapa TEXT  NULL                  ← MapCoordinates de Terry
  created_at       TIMESTAMPTZ DEFAULT now()

── Apartados ──
apartados
  id               INT   PK IDENTITY(1,1)
  lote_id          INT   FK lotes.id  NULL     ← tickets: NULL
  etapa_id         INT   FK etapas.id          ← pool de tickets
  cantidad         INT   DEFAULT 1             ← tickets: cuántos
  inversionista_id INT   FK usuarios.id
  apartado_por     INT   FK usuarios.id NULL
  codigo_asesor    TEXT  FK codigos_asesor
  estatus          TEXT  DEFAULT 'apartado'
  expira_en        TIMESTAMPTZ
  comprobante_url  TEXT  NULL
  solicita_cancelacion BOOL DEFAULT false
  cancel_aprobado_por INT   FK usuarios.id NULL
  cancel_ejecutado_por INT   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) — modalidad lotes
GET/api/proyectos/{id}/ticketsJWTDisponibilidad de tickets (pool) + promociones — modalidad tickets
POST/api/etapas/{id}/promocionesJWT adminCrear promoción por cantidad mínima
DELETE/api/promociones/{id}JWT adminEliminar promoción
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[], inversionista_id, codigo_asesor) — modalidad lotes
POST/api/etapas/{id}/apartar-ticketsJWTApartar cantidad de tickets (body: cantidad, codigo_asesor) — modalidad tickets
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 │ → OTP al correo → lo valida │ → email_validado = true │ → membresía(proyecto, usuario, asesor) creada ┝ ¿Tiene cuenta? │ → login │ → membresía creada al instante └ Tras autenticar → redirige al dashboard 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