Restaurar funcionalidades desde stash + setup de despliegue Coolify

- Recupera el trabajo revertido (doble propuesta, retainer, niveles, API Python) desde el stash de GitHub Desktop
- Dockerfile multi-stage para Next.js (migraciones automáticas al arrancar, seed opcional via RUN_SEED)
- docker-compose.coolify.yml: postgres + web + api con healthchecks y SERVICE_FQDN_*
- Endpoint público /api/health (verifica BD) para healthchecks
- seed.ts parametrizado: conexión DB_* y credenciales SEED_* por entorno (sin passwords hardcodeadas)
- .env.example, .dockerignore, uvicorn con --proxy-headers
- Limpieza: .xlsx y __pycache__ fuera del repo

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
2026-06-10 02:21:18 -06:00
co-authored by Claude Fable 5
parent c3f4449296
commit 69c74faabb
43 changed files with 3536 additions and 1139 deletions
+2
View File
@@ -43,6 +43,7 @@ class ServicioCatalogoBase(BaseModel):
entregablesDefault: list[str] = Field(default_factory=list, description="Lista de entregables")
categoriaId: str = Field(..., min_length=1, description="ID de la categoría")
variante: str | None = None
nivel: str | None = Field(None, description="Nivel/tier: Emprendedor, PYME, Empresario, Corporativo")
orden: int = Field(0, ge=0)
@@ -60,6 +61,7 @@ class ServicioCatalogoUpdate(BaseModel):
entregablesDefault: list[str] | None = None
categoriaId: str | None = None
variante: str | None = None
nivel: str | None = None
activo: bool | None = None
orden: int | None = Field(None, ge=0)
+22 -1
View File
@@ -7,13 +7,28 @@ from pydantic import BaseModel, Field
class ServicioCotizadoInput(BaseModel):
catalogoId: str = Field(..., min_length=1, description="ID del servicio del catálogo")
catalogoId: str = Field(..., min_length=1, description="ID del servicio del catálogo o 'custom-...' para partidas personalizadas")
nombre: str = Field(..., min_length=1, description="Nombre del servicio")
fase: int = Field(..., ge=0, le=3)
tipoPago: str = Field(..., pattern="^(unico|mensual)$")
precio: float = Field(..., ge=0, description="Precio (puede diferir del precioBase)")
tiempoEntrega: str
entregables: list[str] = Field(default_factory=list)
# Partidas por tiempo (sin catálogo). modeloCobro: "fijo" | "horas" | "retainer".
esPersonalizado: bool = False
horas: float | None = Field(None, ge=0)
tarifaHora: float | None = Field(None, ge=0)
modeloCobro: str | None = Field(None, pattern="^(fijo|horas|retainer)$")
montoMinimo: float | None = Field(None, ge=0)
horasIncluidas: float | None = Field(None, ge=0)
# Doble propuesta: "1" | "2" | "ambas". None en cotizaciones normales.
opcion: str | None = Field(None, pattern="^(1|2|ambas)$")
class MetaOpcionInput(BaseModel):
titulo: str | None = None
descripcion: str | None = None
noIncluye: str | None = None
class PlanBucefaloInput(BaseModel):
@@ -38,6 +53,8 @@ class CotizacionCreate(BaseModel):
esquemaPago: str = Field("Pago Unico/Mensual", pattern="^(Pago Unico|Mensual|Pago Unico/Mensual)$")
incluirBonos: bool = False
incluirFinanciamiento: bool = False
esDoble: bool = False
opciones: dict[str, MetaOpcionInput] | None = None
observaciones: str = ""
asesorId: str = Field(..., min_length=1)
cliente: ClienteInput
@@ -92,6 +109,8 @@ class CotizacionUpdate(BaseModel):
esquemaPago: str | None = Field(None, pattern="^(Pago Unico|Mensual|Pago Unico/Mensual)$")
incluirBonos: bool | None = None
incluirFinanciamiento: bool | None = None
esDoble: bool | None = None
opciones: dict[str, MetaOpcionInput] | None = None
observaciones: str | None = None
estado: str | None = Field(None, pattern="^(borrador|enviada|aprobada|rechazada)$")
cliente: ClienteInput | None = None
@@ -120,6 +139,8 @@ class CotizacionResponse(BaseModel):
estado: str
incluirBonos: bool
incluirFinanciamiento: bool
esDoble: bool = False
opcionesMetadata: dict[str, Any] | None = None
observaciones: str | None
clienteId: str
asesorId: str
+16
View File
@@ -10,6 +10,20 @@ class ServicioDraft(BaseModel):
precio: float
tiempoEntrega: str
entregables: list[str] = Field(default_factory=list)
esPersonalizado: bool = False
horas: float | None = None
tarifaHora: float | None = None
modeloCobro: str | None = None
montoMinimo: float | None = None
horasIncluidas: float | None = None
# Doble propuesta: "1" | "2" | "ambas".
opcion: str | None = None
class MetaOpcionDraft(BaseModel):
titulo: str | None = None
descripcion: str | None = None
noIncluye: str | None = None
class ExportDraft(BaseModel):
@@ -25,6 +39,8 @@ class ExportDraft(BaseModel):
servicios: list[ServicioDraft] = Field(default_factory=list)
planBucefaloNivel: str | None = None
observaciones: str = ""
esDoble: bool = False
opciones: dict[str, MetaOpcionDraft] | None = None
class FinanciamientoRequest(BaseModel):