feat: add full project - backend, frontend, docker, specs and configs
This commit is contained in:
parent
b77c7d5a01
commit
e6cb06255b
24489 changed files with 61341 additions and 36 deletions
0
backend/app/schemas/__init__.py
Normal file
0
backend/app/schemas/__init__.py
Normal file
56
backend/app/schemas/agent.py
Normal file
56
backend/app/schemas/agent.py
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, EmailStr, field_validator
|
||||
|
||||
|
||||
class AgentOut(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
name: str
|
||||
photo_url: str | None
|
||||
creci: str
|
||||
email: str
|
||||
phone: str
|
||||
bio: str | None
|
||||
is_active: bool
|
||||
display_order: int
|
||||
|
||||
|
||||
class AgentIn(BaseModel):
|
||||
name: str
|
||||
photo_url: str | None = None
|
||||
creci: str
|
||||
email: str
|
||||
phone: str
|
||||
bio: str | None = None
|
||||
is_active: bool = True
|
||||
display_order: int = 0
|
||||
|
||||
@field_validator("name")
|
||||
@classmethod
|
||||
def name_not_empty(cls, v: str) -> str:
|
||||
if not v.strip():
|
||||
raise ValueError("name não pode ser vazio")
|
||||
return v.strip()
|
||||
|
||||
@field_validator("creci")
|
||||
@classmethod
|
||||
def creci_not_empty(cls, v: str) -> str:
|
||||
if not v.strip():
|
||||
raise ValueError("creci não pode ser vazio")
|
||||
return v.strip()
|
||||
|
||||
@field_validator("email")
|
||||
@classmethod
|
||||
def email_not_empty(cls, v: str) -> str:
|
||||
if not v.strip():
|
||||
raise ValueError("email não pode ser vazio")
|
||||
return v.strip()
|
||||
|
||||
@field_validator("phone")
|
||||
@classmethod
|
||||
def phone_not_empty(cls, v: str) -> str:
|
||||
if not v.strip():
|
||||
raise ValueError("phone não pode ser vazio")
|
||||
return v.strip()
|
||||
68
backend/app/schemas/auth.py
Normal file
68
backend/app/schemas/auth.py
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
from pydantic import BaseModel, EmailStr, field_validator
|
||||
from datetime import datetime, date
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class RegisterIn(BaseModel):
|
||||
name: str
|
||||
email: EmailStr
|
||||
password: str
|
||||
|
||||
# Campos opcionais — enriquecimento de perfil (feature 012)
|
||||
phone: Optional[str] = None
|
||||
whatsapp: Optional[str] = None
|
||||
cpf: Optional[str] = None
|
||||
birth_date: Optional[date] = None
|
||||
address_street: Optional[str] = None
|
||||
address_number: Optional[str] = None
|
||||
address_complement: Optional[str] = None
|
||||
address_neighborhood: Optional[str] = None
|
||||
address_city: Optional[str] = None
|
||||
address_state: Optional[str] = None
|
||||
address_zip: Optional[str] = None
|
||||
|
||||
@field_validator("name")
|
||||
@classmethod
|
||||
def name_not_empty(cls, v: str) -> str:
|
||||
if not v.strip():
|
||||
raise ValueError("Nome não pode ser vazio")
|
||||
if len(v.strip()) < 2:
|
||||
raise ValueError("Nome deve ter pelo menos 2 caracteres")
|
||||
return v.strip()
|
||||
|
||||
@field_validator("email")
|
||||
@classmethod
|
||||
def email_lowercase(cls, v: str) -> str:
|
||||
return v.lower().strip()
|
||||
|
||||
@field_validator("password")
|
||||
@classmethod
|
||||
def password_min_length(cls, v: str) -> str:
|
||||
if len(v) < 8:
|
||||
raise ValueError("Senha deve ter pelo menos 8 caracteres")
|
||||
return v
|
||||
|
||||
|
||||
class LoginIn(BaseModel):
|
||||
email: EmailStr
|
||||
password: str
|
||||
|
||||
@field_validator("email")
|
||||
@classmethod
|
||||
def email_lowercase(cls, v: str) -> str:
|
||||
return v.lower().strip()
|
||||
|
||||
|
||||
class UserOut(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
email: str
|
||||
role: str
|
||||
created_at: datetime
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class AuthTokenOut(BaseModel):
|
||||
access_token: str
|
||||
user: UserOut
|
||||
58
backend/app/schemas/catalog.py
Normal file
58
backend/app/schemas/catalog.py
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
|
||||
class PropertyTypeOut(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
name: str
|
||||
slug: str
|
||||
parent_id: int | None
|
||||
subtypes: list["PropertyTypeOut"] = []
|
||||
property_count: int = 0
|
||||
|
||||
|
||||
# Required for Pydantic v2 to resolve the self-referential forward reference
|
||||
PropertyTypeOut.model_rebuild()
|
||||
|
||||
|
||||
class AmenityOut(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
name: str
|
||||
slug: str
|
||||
group: str
|
||||
|
||||
|
||||
class CityOut(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
name: str
|
||||
slug: str
|
||||
state: str
|
||||
property_count: int = 0
|
||||
|
||||
|
||||
class NeighborhoodOut(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
name: str
|
||||
slug: str
|
||||
city_id: int
|
||||
property_count: int = 0
|
||||
|
||||
|
||||
class ImobiliariaOut(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
name: str
|
||||
logo_url: str | None
|
||||
website: str | None
|
||||
is_active: bool
|
||||
display_order: int
|
||||
95
backend/app/schemas/client_area.py
Normal file
95
backend/app/schemas/client_area.py
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
from pydantic import BaseModel, field_validator
|
||||
from datetime import datetime, date
|
||||
from decimal import Decimal
|
||||
from typing import Optional
|
||||
import uuid
|
||||
|
||||
|
||||
class PropertyBrief(BaseModel):
|
||||
id: str
|
||||
title: str
|
||||
slug: str
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class SavedPropertyOut(BaseModel):
|
||||
id: str
|
||||
property_id: Optional[str]
|
||||
property: Optional[PropertyBrief]
|
||||
created_at: datetime
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class FavoriteIn(BaseModel):
|
||||
property_id: str
|
||||
|
||||
@field_validator("property_id")
|
||||
@classmethod
|
||||
def validate_uuid(cls, v: str) -> str:
|
||||
try:
|
||||
uuid.UUID(v)
|
||||
except ValueError:
|
||||
raise ValueError("property_id deve ser um UUID válido")
|
||||
return v
|
||||
|
||||
|
||||
class VisitRequestOut(BaseModel):
|
||||
id: str
|
||||
property: Optional[PropertyBrief]
|
||||
message: Optional[str]
|
||||
status: str
|
||||
scheduled_at: Optional[datetime]
|
||||
created_at: datetime
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class VisitStatusIn(BaseModel):
|
||||
status: str
|
||||
scheduled_at: Optional[datetime] = None
|
||||
|
||||
@field_validator("status")
|
||||
@classmethod
|
||||
def validate_status(cls, v: str) -> str:
|
||||
allowed = {"pending", "confirmed", "cancelled", "completed"}
|
||||
if v not in allowed:
|
||||
raise ValueError(f'Status deve ser um de: {", ".join(allowed)}')
|
||||
return v
|
||||
|
||||
|
||||
class BoletoOut(BaseModel):
|
||||
id: str
|
||||
property: Optional[PropertyBrief]
|
||||
description: str
|
||||
amount: Decimal
|
||||
due_date: date
|
||||
status: str
|
||||
url: Optional[str]
|
||||
created_at: datetime
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class BoletoCreateIn(BaseModel):
|
||||
user_id: str
|
||||
property_id: Optional[str] = None
|
||||
description: str
|
||||
amount: Decimal
|
||||
due_date: date
|
||||
url: Optional[str] = None
|
||||
|
||||
@field_validator("description")
|
||||
@classmethod
|
||||
def description_not_empty(cls, v: str) -> str:
|
||||
if not v.strip():
|
||||
raise ValueError("Descrição não pode ser vazia")
|
||||
return v.strip()
|
||||
|
||||
@field_validator("amount")
|
||||
@classmethod
|
||||
def amount_positive(cls, v: Decimal) -> Decimal:
|
||||
if v <= 0:
|
||||
raise ValueError("Valor deve ser positivo")
|
||||
return v
|
||||
37
backend/app/schemas/homepage.py
Normal file
37
backend/app/schemas/homepage.py
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, field_validator
|
||||
|
||||
|
||||
class HomepageConfigOut(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
hero_headline: str
|
||||
hero_subheadline: str | None
|
||||
hero_cta_label: str
|
||||
hero_cta_url: str
|
||||
featured_properties_limit: int
|
||||
hero_image_url: str | None = None
|
||||
|
||||
|
||||
class HomepageConfigIn(BaseModel):
|
||||
hero_headline: str
|
||||
hero_subheadline: str | None = None
|
||||
hero_cta_label: str = "Ver Imóveis"
|
||||
hero_cta_url: str = "/imoveis"
|
||||
featured_properties_limit: int = 6
|
||||
hero_image_url: str | None = None
|
||||
|
||||
@field_validator("hero_headline")
|
||||
@classmethod
|
||||
def headline_not_empty(cls, v: str) -> str:
|
||||
if not v.strip():
|
||||
raise ValueError("hero_headline não pode ser vazio")
|
||||
return v
|
||||
|
||||
@field_validator("featured_properties_limit")
|
||||
@classmethod
|
||||
def limit_in_range(cls, v: int) -> int:
|
||||
if not (1 <= v <= 12):
|
||||
raise ValueError("featured_properties_limit deve estar entre 1 e 12")
|
||||
return v
|
||||
64
backend/app/schemas/lead.py
Normal file
64
backend/app/schemas/lead.py
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, field_validator
|
||||
|
||||
|
||||
_EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
|
||||
|
||||
|
||||
class ContactLeadIn(BaseModel):
|
||||
name: str
|
||||
email: str
|
||||
phone: str | None = None
|
||||
message: str
|
||||
|
||||
@field_validator("name")
|
||||
@classmethod
|
||||
def validate_name(cls, v: str) -> str:
|
||||
v = v.strip()
|
||||
if len(v) < 2:
|
||||
raise ValueError("Nome deve ter pelo menos 2 caracteres.")
|
||||
if len(v) > 150:
|
||||
raise ValueError("Nome deve ter no máximo 150 caracteres.")
|
||||
return v
|
||||
|
||||
@field_validator("email")
|
||||
@classmethod
|
||||
def validate_email(cls, v: str) -> str:
|
||||
v = v.strip().lower()
|
||||
if not _EMAIL_RE.match(v):
|
||||
raise ValueError("E-mail inválido.")
|
||||
if len(v) > 254:
|
||||
raise ValueError("E-mail deve ter no máximo 254 caracteres.")
|
||||
return v
|
||||
|
||||
@field_validator("phone")
|
||||
@classmethod
|
||||
def validate_phone(cls, v: str | None) -> str | None:
|
||||
if v is None:
|
||||
return v
|
||||
v = v.strip()
|
||||
if not v:
|
||||
return None
|
||||
if len(v) > 20:
|
||||
raise ValueError("Telefone deve ter no máximo 20 caracteres.")
|
||||
return v
|
||||
|
||||
@field_validator("message")
|
||||
@classmethod
|
||||
def validate_message(cls, v: str) -> str:
|
||||
v = v.strip()
|
||||
if len(v) < 10:
|
||||
raise ValueError("Mensagem deve ter pelo menos 10 caracteres.")
|
||||
if len(v) > 2000:
|
||||
raise ValueError("Mensagem deve ter no máximo 2000 caracteres.")
|
||||
return v
|
||||
|
||||
|
||||
class ContactLeadCreatedOut(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
message: str
|
||||
57
backend/app/schemas/property.py
Normal file
57
backend/app/schemas/property.py
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from uuid import UUID
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
from app.schemas.catalog import AmenityOut, ImobiliariaOut, PropertyTypeOut, CityOut, NeighborhoodOut
|
||||
|
||||
|
||||
class PropertyPhotoOut(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
url: str
|
||||
alt_text: str
|
||||
display_order: int
|
||||
|
||||
|
||||
class PropertyOut(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: UUID
|
||||
title: str
|
||||
slug: str
|
||||
code: str | None = None
|
||||
price: Decimal
|
||||
condo_fee: Decimal | None
|
||||
iptu_anual: Decimal | None = None
|
||||
type: Literal["venda", "aluguel"]
|
||||
subtype: PropertyTypeOut | None
|
||||
bedrooms: int
|
||||
bathrooms: int
|
||||
parking_spots: int
|
||||
area_m2: int
|
||||
city: CityOut | None
|
||||
neighborhood: NeighborhoodOut | None
|
||||
imobiliaria: ImobiliariaOut | None = None
|
||||
is_featured: bool
|
||||
created_at: datetime | None = None
|
||||
photos: list[PropertyPhotoOut]
|
||||
amenities: list[AmenityOut] = []
|
||||
|
||||
|
||||
class PaginatedPropertiesOut(BaseModel):
|
||||
items: list[PropertyOut]
|
||||
total: int
|
||||
page: int
|
||||
per_page: int
|
||||
pages: int
|
||||
|
||||
|
||||
class PropertyDetailOut(PropertyOut):
|
||||
address: str | None = None
|
||||
code: str | None = None
|
||||
description: str | None = None
|
||||
Loading…
Add table
Add a link
Reference in a new issue