feat: Freelancer Match — AI-матчинг, escrow, milestones, portfolio, skill-tests, verification

This commit is contained in:
2026-07-03 15:03:30 +00:00
commit 0b785db1b3
61 changed files with 2725 additions and 0 deletions
+24
View File
@@ -0,0 +1,24 @@
"""Модели SQLAlchemy."""
from app.models.user import User, FreelancerProfile, ClientProfile
from app.models.project import Project
from app.models.proposal import Proposal
from app.models.ai_match import AIMatch
from app.models.escrow import EscrowTransaction
from app.models.milestone import Milestone
from app.models.work_session import WorkSession
from app.models.review import Review
from app.models.message import Message
from app.models.notification import Notification
from app.models.portfolio import PortfolioItem
from app.models.skill_test import SkillTest, SkillTestResult
from app.models.verification import Verification
__all__ = [
"User", "FreelancerProfile", "ClientProfile",
"Project", "Proposal", "AIMatch",
"EscrowTransaction", "Milestone", "WorkSession",
"Review", "Message", "Notification",
"PortfolioItem", "SkillTest", "SkillTestResult",
"Verification",
]
+21
View File
@@ -0,0 +1,21 @@
"""Модель AI-рекомендаций."""
import uuid
from datetime import datetime, timezone
from sqlalchemy import Column, DateTime, Float, ForeignKey, func
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.core.database import Base
class AIMatch(Base):
__tablename__ = "ai_matches"
id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
project_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("projects.id"), nullable=False)
freelancer_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
match_score: Mapped[float] = mapped_column(Float(precision=5, scale=4), nullable=False)
reasons: Mapped[list] = mapped_column("reasons", postgresql.JSONB, default=list)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
+23
View File
@@ -0,0 +1,23 @@
"""Модель Escrow-транзакций (гарант)."""
import uuid
from datetime import datetime, timezone
from sqlalchemy import Column, DateTime, Enum, Float, ForeignKey, func
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.core.database import Base
class EscrowTransaction(Base):
__tablename__ = "escrow_transactions"
id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
project_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("projects.id"), nullable=False)
client_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
freelancer_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
amount: Mapped[float] = mapped_column(Float(precision=10, scale=2), nullable=False)
status: Mapped[str] = mapped_column(Enum("pending", "locked", "released", "disputed", "refunded"), default="pending")
released_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
+21
View File
@@ -0,0 +1,21 @@
"""Модель сообщений чата."""
import uuid
from datetime import datetime, timezone
from sqlalchemy import Column, DateTime, ForeignKey, Text, func
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.core.database import Base
class Message(Base):
__tablename__ = "messages"
id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
work_session_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("work_sessions.id"), nullable=False)
sender_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
content: Mapped[str] = mapped_column(Text, nullable=False)
attachments: Mapped[list | None] = mapped_column("attachments", postgresql.JSONB, default=list)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
+26
View File
@@ -0,0 +1,26 @@
"""Модель Milestone-платежей (Upwork-style)."""
import uuid
from datetime import datetime, timezone
from sqlalchemy import Column, DateTime, Enum, Float, ForeignKey, func
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.core.database import Base
class Milestone(Base):
__tablename__ = "milestones"
id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
project_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("projects.id"), nullable=False)
escrow_transaction_id: Mapped[uuid.UUID | None] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("escrow_transactions.id"))
title: Mapped[str] = mapped_column(String(255), nullable=False)
description: Mapped[str | None] = mapped_column(Text)
amount: Mapped[float] = mapped_column(Float(precision=10, scale=2), nullable=False)
status: Mapped[str] = mapped_column(Enum("pending", "funded", "in_progress", "submitted", "approved", "disputed"), default="pending")
due_date: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
submitted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
approved_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
+22
View File
@@ -0,0 +1,22 @@
"""Модель уведомлений."""
import uuid
from datetime import datetime, timezone
from sqlalchemy import Column, DateTime, Boolean, ForeignKey, String, Text, func
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.core.database import Base
class Notification(Base):
__tablename__ = "notifications"
id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
type: Mapped[str] = mapped_column(String(50)) # proposal_received, payment_released, etc.
title: Mapped[str | None] = mapped_column(String(255))
body: Mapped[str | None] = mapped_column(Text)
is_read: Mapped[bool] = mapped_column(default=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
+23
View File
@@ -0,0 +1,23 @@
"""Модель портфолио фрилансера."""
import uuid
from datetime import datetime, timezone
from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, Text, func
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.core.database import Base
class PortfolioItem(Base):
__tablename__ = "portfolio_items"
id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
freelancer_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
title: Mapped[str] = mapped_column(String(255), nullable=False)
description: Mapped[str | None] = mapped_column(Text)
image_url: Mapped[str | None] = mapped_column(Text) # URL превью работы
live_url: Mapped[str | None] = mapped_column(Text) # Ссылка на работу
technologies: Mapped[list] = mapped_column("technologies", postgresql.JSONB, default=list)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
+47
View File
@@ -0,0 +1,47 @@
"""Модели проектов и заявок."""
import uuid
from datetime import datetime, timezone
from sqlalchemy import Column, DateTime, Enum, Float, ForeignKey, Integer, String, Text, func
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.core.database import Base
class Project(Base):
__tablename__ = "projects"
id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
client_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
title: Mapped[str] = mapped_column(String(255), nullable=False)
description: Mapped[str] = mapped_column(Text, nullable=False)
category: Mapped[str | None] = mapped_column(String(100))
required_skills: Mapped[list] = mapped_column("required_skills", postgresql.JSONB, default=list)
skill_embeddings: Mapped[list | None] = mapped_column("skill_embeddings") # pgvector
budget_min: Mapped[float | None] = mapped_column(Float(precision=10, scale=2))
budget_max: Mapped[float | None] = mapped_column(Float(precision=10, scale=2))
status: Mapped[str] = mapped_column(Enum("open", "in_progress", "completed", "cancelled"), default="open")
deadline: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), onupdate=func.now())
# Relationships
client = relationship("User", foreign_keys=[client_id])
proposals: Mapped[list["Proposal"]] = relationship(back_populates="project")
class Proposal(Base):
__tablename__ = "proposals"
id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
project_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("projects.id"), nullable=False)
freelancer_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
cover_letter: Mapped[str | None] = mapped_column(Text)
proposed_price: Mapped[float | None] = mapped_column(Float(precision=10, scale=2))
estimated_days: Mapped[int | None] = mapped_column(Integer)
status: Mapped[str] = mapped_column(Enum("pending", "accepted", "rejected"), default="pending")
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
project: Mapped["Project"] = relationship(back_populates="proposals")
+22
View File
@@ -0,0 +1,22 @@
"""Модель отзывов."""
import uuid
from datetime import datetime, timezone
from sqlalchemy import Column, DateTime, Enum, ForeignKey, Integer, String, Text, func
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.core.database import Base
class Review(Base):
__tablename__ = "reviews"
id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
project_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("projects.id"), nullable=False)
reviewer_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
reviewee_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
rating: Mapped[int] = mapped_column(Integer, nullable=False)
comment: Mapped[str | None] = mapped_column(Text)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
+33
View File
@@ -0,0 +1,33 @@
"""Модель Skill Tests (сертификация навыков как на Upwork)."""
import uuid
from datetime import datetime, timezone
from sqlalchemy import Column, DateTime, Enum, Float, ForeignKey, Integer, String, Text, func
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.core.database import Base
class SkillTest(Base):
__tablename__ = "skill_tests"
id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
name: Mapped[str] = mapped_column(String(255), nullable=False) # например "Python Basics"
category: Mapped[str] = mapped_column(String(100)) # programming, design, etc.
questions_count: Mapped[int] = mapped_column(Integer, default=40)
passing_score: Mapped[float] = mapped_column(Float(precision=5, scale=2), default=70.0)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
class SkillTestResult(Base):
__tablename__ = "skill_test_results"
id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
skill_test_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("skill_tests.id"), nullable=False)
score: Mapped[float] = mapped_column(Float(precision=5, scale=2))
passed: Mapped[bool] = mapped_column(default=False)
completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
+61
View File
@@ -0,0 +1,61 @@
"""Модели пользователей."""
import uuid
from datetime import datetime, timezone
from sqlalchemy import Column, DateTime, Enum, Float, Integer, String, Text, func
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.core.database import Base
class User(Base):
__tablename__ = "users"
id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
email: Mapped[str] = mapped_column(String(255), unique=True, nullable=False, index=True)
password_hash: Mapped[str | None] = mapped_column(String(255))
full_name: Mapped[str | None] = mapped_column(String(100))
avatar_url: Mapped[str | None] = mapped_column(Text)
role: Mapped[str] = mapped_column(Enum("client", "freelancer", "both"), nullable=False, default="freelancer")
is_verified: Mapped[bool] = mapped_column(default=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), onupdate=func.now())
# Relationships
freelancer_profile: Mapped["FreelancerProfile | None"] = relationship(back_populates="user", uselist=False)
client_profile: Mapped["ClientProfile | None"] = relationship(back_populates="user", uselist=False)
class FreelancerProfile(Base):
__tablename__ = "freelancer_profiles"
user_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, nullable=False)
bio: Mapped[str | None] = mapped_column(Text)
skills: Mapped[list] = mapped_column("skills", postgresql.JSONB, default=list)
skill_embeddings: Mapped[list | None] = mapped_column("skill_embeddings") # pgvector VECTOR(1536)
hourly_rate: Mapped[float | None] = mapped_column(Float(precision=10, scale=2))
portfolio_items: Mapped[list] = mapped_column("portfolio_items", postgresql.JSONB, default=list)
experience_years: Mapped[int | None] = mapped_column(Integer)
languages: Mapped[list] = mapped_column("languages", postgresql.JSONB, default=list)
rating: Mapped[float] = mapped_column(Float(precision=3, scale=2), default=5.0)
total_jobs_completed: Mapped[int] = mapped_column(Integer, default=0)
response_time_hours: Mapped[int | None] = mapped_column(Integer)
is_online: Mapped[bool] = mapped_column(default=False)
last_seen: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
user: Mapped["User"] = relationship(back_populates="freelancer_profile")
class ClientProfile(Base):
__tablename__ = "client_profiles"
user_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, nullable=False)
company_name: Mapped[str | None] = mapped_column(String(255))
industry: Mapped[str | None] = mapped_column(String(100))
budget_range: Mapped[dict | None] = mapped_column("budget_range", postgresql.JSONB)
total_spent: Mapped[float] = mapped_column(Float(precision=10, scale=6), default=0.0)
rating: Mapped[float] = mapped_column(Float(precision=3, scale=2), default=5.0)
user: Mapped["User"] = relationship(back_populates="client_profile")
+27
View File
@@ -0,0 +1,27 @@
"""Модель верификации профиля (Verified Badges)."""
import uuid
from datetime import datetime, timezone
from sqlalchemy import Column, DateTime, Boolean, String, func
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
from sqlalchemy.orm import Mapped, mapped_column
from app.core.database import Base
class Verification(Base):
__tablename__ = "verifications"
id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), nullable=False, unique=True)
# Типы верификации
is_email_verified: Mapped[bool] = mapped_column(default=False)
is_phone_verified: Mapped[bool] = mapped_column(default=False)
is_id_verified: Mapped[bool] = mapped_column(default=False) # ID document
is_bank_verified: Mapped[bool] = mapped_column(default=False) # Bank account
verified_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
+24
View File
@@ -0,0 +1,24 @@
"""Модель рабочей сессии."""
import uuid
from datetime import datetime, timezone
from sqlalchemy import Column, DateTime, Enum, Float, ForeignKey, func
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.core.database import Base
class WorkSession(Base):
__tablename__ = "work_sessions"
id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
project_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("projects.id"), nullable=False)
freelancer_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
client_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
start_time: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
end_time: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
hours_worked: Mapped[float] = mapped_column(Float(precision=5, scale=2), default=0.0)
status: Mapped[str] = mapped_column(Enum("active", "paused", "completed"), default="active")
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())