# Data Model: Trabalhe Conosco **Feature**: 028-trabalhe-conosco **Phase**: 1 — Design & Contracts **Source**: spec.md --- ## Entidade: JobApplication (Candidatura) ### Tabela: `job_applications` | Coluna | Tipo SQL | Nullable | Default | Restrições | |------------------|---------------------------------|----------|------------------|--------------------------------------------| | `id` | `SERIAL` (INTEGER PK) | NOT NULL | auto-increment | PRIMARY KEY | | `name` | `VARCHAR(150)` | NOT NULL | — | campo obrigatório | | `email` | `VARCHAR(254)` | NOT NULL | — | formato e-mail válido (validado no backend)| | `phone` | `VARCHAR(30)` | NULL | — | opcional conforme spec | | `role_interest` | `VARCHAR(100)` | NOT NULL | — | enum: Corretor(a), Assistente Administrativo, Estagiário(a), Outro | | `message` | `TEXT` | NOT NULL | — | apresentação/mensagem do candidato | | `file_name` | `VARCHAR(255)` | NULL | — | nome do arquivo de currículo (sem upload) | | `status` | `VARCHAR(50)` | NOT NULL | `'pending'` | estado da candidatura (pending / reviewed) | | `created_at` | `TIMESTAMP WITHOUT TIME ZONE` | NOT NULL | `now()` (server) | imutável após criação | ### Índices | Índice | Colunas | Motivo | |------------------------------------|-----------------|------------------------------------------------| | `ix_job_applications_created_at` | `created_at` | ordenação DESC na listagem admin | | `ix_job_applications_status` | `status` | filtragem futura por estado | ### Invariantes 1. `name`, `email`, `role_interest` e `message` nunca são deixados em branco (validação Pydantic). 2. `email` deve ser validado com `pydantic.EmailStr` — formato RFC-5321. 3. `role_interest` deve ser um dos valores permitidos: `"Corretor(a)"`, `"Assistente Administrativo"`, `"Estagiário(a)"`, `"Outro"`. 4. `message` não pode ultrapassar 5000 caracteres (validação frontend + Pydantic `max_length`). 5. `phone` é opcional — sem validação de formato nesta versão. 6. `file_name` armazena apenas o nome do arquivo informado, sem conteúdo binário. 7. Múltiplas candidaturas do mesmo `email` são permitidas (sem deduplicação nesta versão). 8. Nenhum `DELETE` físico é exposto; o campo `status` permite rastreabilidade futura. ### Diagrama ER ``` job_applications ├── id PK SERIAL ├── name VARCHAR(150) NOT NULL ├── email VARCHAR(254) NOT NULL ├── phone VARCHAR(30) NULL ├── role_interest VARCHAR(100) NOT NULL ├── message TEXT NOT NULL ├── file_name VARCHAR(255) NULL ├── status VARCHAR(50) NOT NULL DEFAULT 'pending' └── created_at TIMESTAMP NOT NULL DEFAULT now() ``` Sem relacionamentos com outras tabelas nesta versão. Entidade standalone. --- ## Modelo SQLAlchemy: `backend/app/models/job_application.py` ```python from app.extensions import db ROLE_INTEREST_OPTIONS = [ "Corretor(a)", "Assistente Administrativo", "Estagiário(a)", "Outro", ] class JobApplication(db.Model): __tablename__ = "job_applications" id = db.Column(db.Integer, primary_key=True, autoincrement=True) name = db.Column(db.String(150), nullable=False) email = db.Column(db.String(254), nullable=False) phone = db.Column(db.String(30), nullable=True) role_interest = db.Column(db.String(100), nullable=False) message = db.Column(db.Text, nullable=False) file_name = db.Column(db.String(255), nullable=True) status = db.Column(db.String(50), nullable=False, default="pending") created_at = db.Column( db.DateTime, nullable=False, server_default=db.func.now() ) def __repr__(self) -> str: return f"" ``` --- ## Migration Alembic: `backend/migrations/versions/i1j2k3l4m5n6_add_job_applications.py` ```python """add job_applications table Revision ID: i1j2k3l4m5n6 Revises: h1i2j3k4l5m6 Create Date: 2026-04-21 00:00:00.000000 """ import sqlalchemy as sa from alembic import op revision = "i1j2k3l4m5n6" down_revision = "h1i2j3k4l5m6" branch_labels = None depends_on = None def upgrade(): op.create_table( "job_applications", sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), sa.Column("name", sa.String(length=150), nullable=False), sa.Column("email", sa.String(length=254), nullable=False), sa.Column("phone", sa.String(length=30), nullable=True), sa.Column("role_interest", sa.String(length=100), nullable=False), sa.Column("message", sa.Text(), nullable=False), sa.Column("file_name", sa.String(length=255), nullable=True), sa.Column( "status", sa.String(length=50), nullable=False, server_default=sa.text("'pending'"), ), sa.Column( "created_at", sa.DateTime(), nullable=False, server_default=sa.func.now(), ), sa.PrimaryKeyConstraint("id"), ) op.create_index( "ix_job_applications_created_at", "job_applications", ["created_at"] ) op.create_index( "ix_job_applications_status", "job_applications", ["status"] ) def downgrade(): op.drop_index("ix_job_applications_status", table_name="job_applications") op.drop_index("ix_job_applications_created_at", table_name="job_applications") op.drop_table("job_applications") ``` --- ## Schemas Pydantic: `backend/app/schemas/job_application.py` ```python from __future__ import annotations from datetime import datetime from pydantic import BaseModel, ConfigDict, EmailStr, field_validator VALID_ROLES = {"Corretor(a)", "Assistente Administrativo", "Estagiário(a)", "Outro"} class JobApplicationIn(BaseModel): name: str email: EmailStr phone: str | None = None role_interest: str message: str file_name: str | None = None @field_validator("name") @classmethod def name_not_empty(cls, v: str) -> str: v = v.strip() if not v: raise ValueError("name não pode ser vazio") return v @field_validator("role_interest") @classmethod def role_must_be_valid(cls, v: str) -> str: if v not in VALID_ROLES: raise ValueError(f"role_interest deve ser um de: {', '.join(sorted(VALID_ROLES))}") return v @field_validator("message") @classmethod def message_not_empty(cls, v: str) -> str: v = v.strip() if not v: raise ValueError("message não pode ser vazia") if len(v) > 5000: raise ValueError("message não pode ultrapassar 5000 caracteres") return v class JobApplicationOut(BaseModel): model_config = ConfigDict(from_attributes=True) id: int name: str email: str phone: str | None role_interest: str message: str file_name: str | None status: str created_at: datetime ```