🎯 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)
- Pagos digitales / pasarela (Stripe/OpenPay) — la validación es manual (comprobante + conciliador)
- Reportes descargables
- Mapa interactivo SVG de lotes
- Planes de suscripción / facturación (anotado como futuro)
- Migración de datos desde Terry 1/2
- Auditoría / historial de lotes (pendiente para v2)
👤 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
| Rol | Cómo se obtiene |
| 👑 Admin | Se registra en la plataforma y crea un proyecto |
| 🤝 Asesor | Entra por enlace de invitación del proyecto (código de proyecto) |
| 🧾 Conciliador | Asesor con permisos extra; el Admin lo asigna dentro del equipo del proyecto |
| 💼 Inversionista | Entra 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ón | Resultado |
| Crear un proyecto | Eres Admin de ese proyecto |
| Aceptar invitación (código de proyecto) | Eres Asesor de ese proyecto |
| El Admin del proyecto te asigna conciliador | Eres Conciliador en ese proyecto |
| Apartar un lote | Eres 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
| Tipo | Generado por | Para | Persistencia |
| 🔵 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
| Tipo | URL de ejemplo |
| Para asesores | terry3.pages.dev/unirse?codigo=proj_a7b3x9k2 |
| Para inversionistas | terry3.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
| Estado | Comportamiento |
| Activo | Visible para asesores e inversionistas. Se puede apartar. |
| Inactivo | Muestra "Proyecto en pausa, pronto estará disponible." No se puede apartar. Asesores existentes siguen viéndolo. |
| Cerrado | Muestra "Proyecto vendido en su totalidad. Gracias." Sin operaciones. |
Lote
disponible ──→ apartado ──→ regular ──→ pagado
│ │
↓ ↓
(vence sin (mora)
comprobante)
↓
disponible
📡 Endpoints
| Método | Ruta | Auth | Descripción |
| POST | /api/auth/registro | — | Registrar cuenta (cualquier usuario) |
| POST | /api/auth/login | — | Login email + contraseña |
| POST | /api/auth/refresh | refresh | Renovar 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/proyectos | JWT | Mis proyectos (como admin y como miembro) |
| POST | /api/proyectos | JWT | Crear proyecto |
| PATCH | /api/proyectos/{id} | JWT admin | Editar / cambiar estatus |
| GET | /api/proyectos/{id} | JWT | Detalle del proyecto |
| GET | /api/proyectos/{id}/etapas | JWT | Etapas del proyecto |
| PATCH | /api/proyectos/{id}/etapa-activa | JWT admin | Cambiar etapa en venta |
| GET | /api/proyectos/{id}/lotes | JWT | Lotes con filtros (etapa, estatus) |
| POST | /api/proyectos/{id}/codigo-asesor | JWT | Generar código de asesor para inversionistas |
| GET | /api/proyectos/{id}/equipo | JWT admin | Ver colaboradores |
| PATCH | /api/proyectos/{id}/equipo/{usuario} | JWT admin | Asignar/quitar conciliador |
| POST | /api/lotes/apartar | JWT | Apartar uno o varios lotes (body: lotes[], codigo_asesor) |
| POST | /api/lotes/{id}/comprobante | JWT | Subir comprobante de pago |
| PATCH | /api/lotes/{id}/estatus | JWT conciliador/admin | Cambiar estatus |
| POST | /api/lotes/{id}/solicitar-cancelacion | JWT | Inversionista solicita cancelación |
| POST | /api/lotes/{id}/aprobar-cancelacion | JWT asesor | Asesor da visto bueno |
| POST | /api/lotes/{id}/ejecutar-cancelacion | JWT conciliador | Conciliador ejecuta cancelación |
| POST | /api/lotes/liberar-expirados | JWT admin | Liberar 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
- JWT: access corto (30 min) + refresh (rotación, revocable)
- Inversionista: sin contraseña — email + OTP + flag email_validado
- Contraseñas con bcrypt
- OTP: 6 dígitos, expira 15 min
- Códigos de invitación: tokens aleatorios no secuenciales (GUID o nanoid)
- Código de proyecto: fijo y permanente (nunca cambia)
- Rate limit en login/OTP (5 intentos/IP/minuto)
- CORS para frontend en Cloudflare Pages
- Control de concurrencia en apartados: transacciones con
SELECT ... FOR UPDATE en PostgreSQL
- Solo se aparta para cuentas verificadas (email existe + validado)
🌐 Dominio y Deploy
| Recurso | URL |
| Spec visual | terry3-spec.pages.dev ← estás aquí |
| API | Dokploy — dominio por asignar (sslip.io) |
| Frontend | terry3.pages.dev (pendiente) |
| Base de datos | PostgreSQL 18 en Dokploy — base terry3 |
Deploy
- API: FastAPI en contenedor Docker → Dokploy. Env vars: DATABASE_URL, JWT_SECRET, SMTP_HOST, SMTP_USER, SMTP_PASS, CORS_ORIGIN
- Frontend: React + Vite + Tailwind → Cloudflare Pages
- Spec: Cloudflare Pages
terry3-spec
- Migraciones: Alembic (suben con el contenedor)
✅ Decisiones cerradas
| Fecha | Decisión | Motivo |
| 2026-11-05 | Stack: FastAPI + PostgreSQL 18 + JWT + Alembic | Validable aquí (no hay .NET) |
| 2026-11-05 | Sin capa Organización; cada cuenta Admin es su propio tenant | Simplificación |
| 2026-11-05 | Permisos por membresía (usuario × proyecto), no rol global | Admin=asesor de otro, conciliador solo en un proyecto |
| 2026-11-05 | Registro unificado — permisos los da la acción, no el formulario | Resuelve asesor→admin naturalmente |
| 2026-11-05 | Todos pueden comprar; internos apartan para terceros | Requisito de Jorge |
| 2026-11-05 | Dos tipos de código: proyecto (asesores, fijo) y asesor (inversionistas) | Flujo de negocio real |
| 2026-11-05 | Inversionista: email + OTP (1ª vez) → solo email (siguientes) | Mínima fricción |
| 2026-11-05 | Código de proyecto: fijo, nunca cambia | No romper asociaciones |
| 2026-11-05 | Código de asesor: no expira, permanente | Asigna comisión al asesor |
| 2026-11-05 | Admin también genera código de asesor | Consistente: todos promocionan |
| 2026-11-05 | Desvinculación: la decide el asesor, no el admin | Autogestión |
| 2026-11-05 | Solo se aparta para cuentas verificadas | Evita apartados fantasmas |
| 2026-11-05 | Cancelación: solicitud → asesor → conciliador | Flujo con supervisión |
| 2026-11-05 | Selección múltiple de lotes + carrito + revalidación | Como en Terry actual |
| 2026-11-05 | Conciliador v1 = asesor + cambio de estatus (sin pagos) | Alcance acotado |
| 2026-11-05 | Estados de proyecto: activo / inactivo / cerrado | Control del admin |
| 2026-11-05 | Apartados con expiración por etapa + comprobante | Flujo real de compra-venta |
| 2026-11-05 | Frontend React + Vite + Tailwind; API separada | Decisión de Jorge |
| 2026-11-05 | Empezar de cero, sin migrar | — |
📋 Pendientes para v2
- Historial / auditoría de lotes — pendiente de definir formato
- Almacenamiento de comprobantes — se define cuando se implemente
- Lista exacta de plantillas de correo a definir en implementación
🧭 Próximos pasos
- ✅ Spec v1.0 aprobado (estás aquí)
- ⬜ Crear repositorio
terry3 en GitHub
- ⬜ Crear base
terry3 en PostgreSQL 18 (Dokploy)
- ⬜ API + Alembic + migraciones
- ⬜ Lógica de auth (JWT + OTP + códigos de invitación)
- ⬜ CRUD de proyectos, etapas, lotes
- ⬜ Flujo de apartado con concurrencia
- ⬜ Frontend React
- ⬜ Registrar en
_REFERENCIA.md y tarjeta en el taller
- ⬜ Despliegue completo