feat: add full project - backend, frontend, docker, specs and configs

This commit is contained in:
MatheusAlves96 2026-04-20 23:59:45 -03:00
parent b77c7d5a01
commit e6cb06255b
24489 changed files with 61341 additions and 36 deletions

View file

View 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()

View 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

View 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

View 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

View 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

View 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

View 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