chore: sync uncommitted working tree changes before clone cleanup
This commit is contained in:
+12
-6
@@ -2,8 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Response, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
@@ -14,9 +12,17 @@ from app.models.user import User
|
||||
from app.schemas.account import AccountCreate, AccountOut, AccountUpdate
|
||||
from app.services.account_service import (
|
||||
create_account as svc_create_account,
|
||||
)
|
||||
from app.services.account_service import (
|
||||
get_account as svc_get_account,
|
||||
)
|
||||
from app.services.account_service import (
|
||||
list_accounts as svc_list_accounts,
|
||||
)
|
||||
from app.services.account_service import (
|
||||
soft_delete_account as svc_soft_delete_account,
|
||||
)
|
||||
from app.services.account_service import (
|
||||
update_account as svc_update_account,
|
||||
)
|
||||
|
||||
@@ -48,10 +54,10 @@ async def create_account(
|
||||
async def list_accounts(
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(20, ge=1, le=100),
|
||||
industry: Optional[Industry] = None,
|
||||
size: Optional[AccountSize] = None,
|
||||
owner_id: Optional[int] = None,
|
||||
q: Optional[str] = None,
|
||||
industry: Industry | None = None,
|
||||
size: AccountSize | None = None,
|
||||
owner_id: int | None = None,
|
||||
q: str | None = None,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> list[AccountOut]:
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Response, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
@@ -19,11 +17,23 @@ from app.schemas.activity import (
|
||||
)
|
||||
from app.services.activity_service import (
|
||||
NoParentException,
|
||||
)
|
||||
from app.services.activity_service import (
|
||||
complete_activity as svc_complete_activity,
|
||||
)
|
||||
from app.services.activity_service import (
|
||||
create_activity as svc_create_activity,
|
||||
)
|
||||
from app.services.activity_service import (
|
||||
get_activity as svc_get_activity,
|
||||
)
|
||||
from app.services.activity_service import (
|
||||
list_activities as svc_list_activities,
|
||||
)
|
||||
from app.services.activity_service import (
|
||||
soft_delete_activity as svc_soft_delete_activity,
|
||||
)
|
||||
from app.services.activity_service import (
|
||||
update_activity as svc_update_activity,
|
||||
)
|
||||
|
||||
@@ -58,10 +68,10 @@ async def create_activity(
|
||||
async def list_activities(
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(20, ge=1, le=100),
|
||||
type: Optional[ActivityType] = None,
|
||||
owner_id: Optional[int] = None,
|
||||
overdue: Optional[bool] = None,
|
||||
completed: Optional[bool] = None,
|
||||
type: ActivityType | None = None,
|
||||
owner_id: int | None = None,
|
||||
overdue: bool | None = None,
|
||||
completed: bool | None = None,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> list[ActivityOut]:
|
||||
|
||||
+1
-1
@@ -7,8 +7,8 @@ from fastapi.security import OAuth2PasswordRequestForm
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.config import get_settings
|
||||
from app.core.deps import get_current_user
|
||||
from app.core.db import get_db
|
||||
from app.core.deps import get_current_user
|
||||
from app.models.user import User
|
||||
from app.schemas.auth import (
|
||||
LogoutResponse,
|
||||
|
||||
+13
-5
@@ -2,8 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Response, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
@@ -13,10 +11,20 @@ from app.models.user import User
|
||||
from app.schemas.contact import ContactCreate, ContactOut, ContactUpdate
|
||||
from app.services.contact_service import (
|
||||
InvalidAccount,
|
||||
)
|
||||
from app.services.contact_service import (
|
||||
create_contact as svc_create_contact,
|
||||
)
|
||||
from app.services.contact_service import (
|
||||
get_contact as svc_get_contact,
|
||||
)
|
||||
from app.services.contact_service import (
|
||||
list_contacts as svc_list_contacts,
|
||||
)
|
||||
from app.services.contact_service import (
|
||||
soft_delete_contact as svc_soft_delete_contact,
|
||||
)
|
||||
from app.services.contact_service import (
|
||||
update_contact as svc_update_contact,
|
||||
)
|
||||
|
||||
@@ -51,9 +59,9 @@ async def create_contact(
|
||||
async def list_contacts(
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(20, ge=1, le=100),
|
||||
account_id: Optional[int] = None,
|
||||
owner_id: Optional[int] = None,
|
||||
q: Optional[str] = None,
|
||||
account_id: int | None = None,
|
||||
owner_id: int | None = None,
|
||||
q: str | None = None,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> list[ContactOut]:
|
||||
|
||||
+17
-7
@@ -2,8 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Response, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
@@ -20,12 +18,24 @@ from app.schemas.deal import (
|
||||
)
|
||||
from app.services.deal_service import (
|
||||
InvalidAccount,
|
||||
create_deal as svc_create_deal,
|
||||
get_deal as svc_get_deal,
|
||||
get_pipeline,
|
||||
)
|
||||
from app.services.deal_service import (
|
||||
create_deal as svc_create_deal,
|
||||
)
|
||||
from app.services.deal_service import (
|
||||
get_deal as svc_get_deal,
|
||||
)
|
||||
from app.services.deal_service import (
|
||||
list_deals as svc_list_deals,
|
||||
)
|
||||
from app.services.deal_service import (
|
||||
soft_delete_deal as svc_soft_delete_deal,
|
||||
)
|
||||
from app.services.deal_service import (
|
||||
update_deal as svc_update_deal,
|
||||
)
|
||||
from app.services.deal_service import (
|
||||
update_stage as svc_update_stage,
|
||||
)
|
||||
|
||||
@@ -60,9 +70,9 @@ async def create_deal(
|
||||
async def list_deals(
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(20, ge=1, le=100),
|
||||
stage: Optional[DealStage] = None,
|
||||
owner_id: Optional[int] = None,
|
||||
account_id: Optional[int] = None,
|
||||
stage: DealStage | None = None,
|
||||
owner_id: int | None = None,
|
||||
account_id: int | None = None,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> list[DealOut]:
|
||||
|
||||
+12
-4
@@ -2,8 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Response, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
@@ -14,10 +12,20 @@ from app.models.user import User
|
||||
from app.schemas.note import NoteCreate, NoteOut, NoteUpdate
|
||||
from app.services.note_service import (
|
||||
InvalidParent,
|
||||
)
|
||||
from app.services.note_service import (
|
||||
create_note as svc_create_note,
|
||||
)
|
||||
from app.services.note_service import (
|
||||
get_note as svc_get_note,
|
||||
)
|
||||
from app.services.note_service import (
|
||||
list_notes as svc_list_notes,
|
||||
)
|
||||
from app.services.note_service import (
|
||||
soft_delete_note as svc_soft_delete_note,
|
||||
)
|
||||
from app.services.note_service import (
|
||||
update_note as svc_update_note,
|
||||
)
|
||||
|
||||
@@ -52,8 +60,8 @@ async def create_note(
|
||||
async def list_notes(
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(20, ge=1, le=100),
|
||||
parent_type: Optional[NoteParentType] = None,
|
||||
parent_id: Optional[int] = None,
|
||||
parent_type: NoteParentType | None = None,
|
||||
parent_id: int | None = None,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> list[NoteOut]:
|
||||
|
||||
@@ -13,9 +13,17 @@ from app.services.tag_service import (
|
||||
DuplicateTagLink,
|
||||
InvalidParent,
|
||||
TagNotFound,
|
||||
)
|
||||
from app.services.tag_service import (
|
||||
create_tag as svc_create_tag,
|
||||
)
|
||||
from app.services.tag_service import (
|
||||
link_tag as svc_link_tag,
|
||||
)
|
||||
from app.services.tag_service import (
|
||||
list_tags as svc_list_tags,
|
||||
)
|
||||
from app.services.tag_service import (
|
||||
unlink_tag as svc_unlink_tag,
|
||||
)
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import FastAPI, Request, status
|
||||
|
||||
+13
-13
@@ -3,7 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING, Any, Optional
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from sqlalchemy import JSON, ForeignKey, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
@@ -11,12 +11,12 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from app.models.base import Base, OrgScopedMixin, SoftDeleteMixin, TimestampMixin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from app.models.user import User
|
||||
from app.models.activity import Activity
|
||||
from app.models.contact import Contact
|
||||
from app.models.deal import Deal
|
||||
from app.models.activity import Activity
|
||||
from app.models.note import Note
|
||||
from app.models.tag_link import TagLink
|
||||
from app.models.user import User
|
||||
|
||||
|
||||
class Industry(str, Enum):
|
||||
@@ -43,36 +43,36 @@ class Account(Base, TimestampMixin, SoftDeleteMixin, OrgScopedMixin):
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||
name: Mapped[str] = mapped_column(String(255), nullable=False, index=True)
|
||||
website: Mapped[Optional[str]] = mapped_column(String(512), nullable=True)
|
||||
industry: Mapped[Optional[Industry]] = mapped_column(
|
||||
website: Mapped[str | None] = mapped_column(String(512), nullable=True)
|
||||
industry: Mapped[Industry | None] = mapped_column(
|
||||
String(32), nullable=True, index=True
|
||||
)
|
||||
size: Mapped[Optional[AccountSize]] = mapped_column(String(32), nullable=True)
|
||||
address: Mapped[Optional[dict[str, Any]]] = mapped_column(JSON, nullable=True)
|
||||
size: Mapped[AccountSize | None] = mapped_column(String(32), nullable=True)
|
||||
address: Mapped[dict[str, Any] | None] = mapped_column(JSON, nullable=True)
|
||||
owner_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("users.id"), nullable=False, index=True
|
||||
)
|
||||
|
||||
# Relationships
|
||||
owner: Mapped["User"] = relationship(
|
||||
owner: Mapped[User] = relationship(
|
||||
"User", foreign_keys=[owner_id], lazy="joined"
|
||||
)
|
||||
contacts: Mapped[list["Contact"]] = relationship(
|
||||
contacts: Mapped[list[Contact]] = relationship(
|
||||
"Contact", back_populates="account", lazy="selectin"
|
||||
)
|
||||
deals: Mapped[list["Deal"]] = relationship(
|
||||
deals: Mapped[list[Deal]] = relationship(
|
||||
"Deal", back_populates="account", lazy="selectin"
|
||||
)
|
||||
activities: Mapped[list["Activity"]] = relationship(
|
||||
activities: Mapped[list[Activity]] = relationship(
|
||||
"Activity", back_populates="account", lazy="selectin"
|
||||
)
|
||||
notes: Mapped[list["Note"]] = relationship(
|
||||
notes: Mapped[list[Note]] = relationship(
|
||||
"Note",
|
||||
primaryjoin="and_(Account.id==foreign(Note.parent_id), Note.parent_type=='account')",
|
||||
viewonly=True,
|
||||
lazy="selectin",
|
||||
)
|
||||
tags: Mapped[list["TagLink"]] = relationship(
|
||||
tags: Mapped[list[TagLink]] = relationship(
|
||||
"TagLink",
|
||||
primaryjoin="and_(Account.id==foreign(TagLink.parent_id), TagLink.parent_type=='account')",
|
||||
viewonly=True,
|
||||
|
||||
+12
-12
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
@@ -12,10 +12,10 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from app.models.base import Base, OrgScopedMixin, SoftDeleteMixin, TimestampMixin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from app.models.user import User
|
||||
from app.models.account import Account
|
||||
from app.models.contact import Contact
|
||||
from app.models.deal import Deal
|
||||
from app.models.user import User
|
||||
|
||||
|
||||
class ActivityType(str, Enum):
|
||||
@@ -36,20 +36,20 @@ class Activity(Base, TimestampMixin, SoftDeleteMixin, OrgScopedMixin):
|
||||
String(32), nullable=False, index=True
|
||||
)
|
||||
subject: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
body: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
||||
due_date: Mapped[Optional[datetime]] = mapped_column(
|
||||
body: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
due_date: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True, index=True
|
||||
)
|
||||
completed_at: Mapped[Optional[datetime]] = mapped_column(
|
||||
completed_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True
|
||||
)
|
||||
account_id: Mapped[Optional[int]] = mapped_column(
|
||||
account_id: Mapped[int | None] = mapped_column(
|
||||
ForeignKey("accounts.id"), nullable=True, index=True
|
||||
)
|
||||
contact_id: Mapped[Optional[int]] = mapped_column(
|
||||
contact_id: Mapped[int | None] = mapped_column(
|
||||
ForeignKey("contacts.id"), nullable=True, index=True
|
||||
)
|
||||
deal_id: Mapped[Optional[int]] = mapped_column(
|
||||
deal_id: Mapped[int | None] = mapped_column(
|
||||
ForeignKey("deals.id"), nullable=True, index=True
|
||||
)
|
||||
owner_id: Mapped[int] = mapped_column(
|
||||
@@ -57,16 +57,16 @@ class Activity(Base, TimestampMixin, SoftDeleteMixin, OrgScopedMixin):
|
||||
)
|
||||
|
||||
# Relationships
|
||||
account: Mapped[Optional["Account"]] = relationship(
|
||||
account: Mapped[Account | None] = relationship(
|
||||
"Account", back_populates="activities", lazy="selectin"
|
||||
)
|
||||
contact: Mapped[Optional["Contact"]] = relationship(
|
||||
contact: Mapped[Contact | None] = relationship(
|
||||
"Contact", back_populates="activities", lazy="selectin"
|
||||
)
|
||||
deal: Mapped[Optional["Deal"]] = relationship(
|
||||
deal: Mapped[Deal | None] = relationship(
|
||||
"Deal", back_populates="activities", lazy="selectin"
|
||||
)
|
||||
owner: Mapped["User"] = relationship(
|
||||
owner: Mapped[User] = relationship(
|
||||
"User", foreign_keys=[owner_id], lazy="joined"
|
||||
)
|
||||
|
||||
|
||||
+1
-2
@@ -3,7 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, func
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
@@ -31,7 +30,7 @@ class TimestampMixin:
|
||||
class SoftDeleteMixin:
|
||||
"""Adds deleted_at column for soft-delete pattern."""
|
||||
|
||||
deleted_at: Mapped[Optional[datetime]] = mapped_column(
|
||||
deleted_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True),
|
||||
nullable=True,
|
||||
default=None,
|
||||
|
||||
+10
-10
@@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import ForeignKey, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
@@ -10,11 +10,11 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from app.models.base import Base, OrgScopedMixin, SoftDeleteMixin, TimestampMixin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from app.models.user import User
|
||||
from app.models.account import Account
|
||||
from app.models.activity import Activity
|
||||
from app.models.note import Note
|
||||
from app.models.tag_link import TagLink
|
||||
from app.models.user import User
|
||||
|
||||
|
||||
class Contact(Base, TimestampMixin, SoftDeleteMixin, OrgScopedMixin):
|
||||
@@ -23,9 +23,9 @@ class Contact(Base, TimestampMixin, SoftDeleteMixin, OrgScopedMixin):
|
||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||
first_name: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||
last_name: Mapped[str] = mapped_column(String(128), nullable=False, index=True)
|
||||
email: Mapped[Optional[str]] = mapped_column(String(255), nullable=True, index=True)
|
||||
phone: Mapped[Optional[str]] = mapped_column(String(64), nullable=True)
|
||||
account_id: Mapped[Optional[int]] = mapped_column(
|
||||
email: Mapped[str | None] = mapped_column(String(255), nullable=True, index=True)
|
||||
phone: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||||
account_id: Mapped[int | None] = mapped_column(
|
||||
ForeignKey("accounts.id"), nullable=True, index=True
|
||||
)
|
||||
owner_id: Mapped[int] = mapped_column(
|
||||
@@ -33,22 +33,22 @@ class Contact(Base, TimestampMixin, SoftDeleteMixin, OrgScopedMixin):
|
||||
)
|
||||
|
||||
# Relationships
|
||||
account: Mapped[Optional["Account"]] = relationship(
|
||||
account: Mapped[Account | None] = relationship(
|
||||
"Account", back_populates="contacts", lazy="selectin"
|
||||
)
|
||||
owner: Mapped["User"] = relationship(
|
||||
owner: Mapped[User] = relationship(
|
||||
"User", foreign_keys=[owner_id], lazy="joined"
|
||||
)
|
||||
activities: Mapped[list["Activity"]] = relationship(
|
||||
activities: Mapped[list[Activity]] = relationship(
|
||||
"Activity", back_populates="contact", lazy="selectin"
|
||||
)
|
||||
notes: Mapped[list["Note"]] = relationship(
|
||||
notes: Mapped[list[Note]] = relationship(
|
||||
"Note",
|
||||
primaryjoin="and_(Contact.id==foreign(Note.parent_id), Note.parent_type=='contact')",
|
||||
viewonly=True,
|
||||
lazy="selectin",
|
||||
)
|
||||
tags: Mapped[list["TagLink"]] = relationship(
|
||||
tags: Mapped[list[TagLink]] = relationship(
|
||||
"TagLink",
|
||||
primaryjoin="and_(Contact.id==foreign(TagLink.parent_id), TagLink.parent_type=='contact')",
|
||||
viewonly=True,
|
||||
|
||||
+11
-11
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
from datetime import date
|
||||
from decimal import Decimal
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import Date, ForeignKey, Numeric, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
@@ -13,12 +13,12 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from app.models.base import Base, OrgScopedMixin, SoftDeleteMixin, TimestampMixin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from app.models.user import User
|
||||
from app.models.account import Account
|
||||
from app.models.activity import Activity
|
||||
from app.models.deal_stage_history import DealStageHistory
|
||||
from app.models.note import Note
|
||||
from app.models.tag_link import TagLink
|
||||
from app.models.deal_stage_history import DealStageHistory
|
||||
from app.models.user import User
|
||||
|
||||
|
||||
class DealStage(str, Enum):
|
||||
@@ -48,39 +48,39 @@ class Deal(Base, TimestampMixin, SoftDeleteMixin, OrgScopedMixin):
|
||||
server_default=DealStage.lead.value,
|
||||
index=True,
|
||||
)
|
||||
close_date: Mapped[Optional[date]] = mapped_column(Date, nullable=True)
|
||||
close_date: Mapped[date | None] = mapped_column(Date, nullable=True)
|
||||
account_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("accounts.id"), nullable=False, index=True
|
||||
)
|
||||
owner_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("users.id"), nullable=False, index=True
|
||||
)
|
||||
won_lost_reason: Mapped[Optional[str]] = mapped_column(String(512), nullable=True)
|
||||
won_lost_reason: Mapped[str | None] = mapped_column(String(512), nullable=True)
|
||||
|
||||
# Relationships
|
||||
account: Mapped["Account"] = relationship(
|
||||
account: Mapped[Account] = relationship(
|
||||
"Account", back_populates="deals", lazy="selectin"
|
||||
)
|
||||
owner: Mapped["User"] = relationship(
|
||||
owner: Mapped[User] = relationship(
|
||||
"User", foreign_keys=[owner_id], lazy="joined"
|
||||
)
|
||||
stage_history: Mapped[list["DealStageHistory"]] = relationship(
|
||||
stage_history: Mapped[list[DealStageHistory]] = relationship(
|
||||
"DealStageHistory",
|
||||
back_populates="deal",
|
||||
cascade="all, delete-orphan",
|
||||
lazy="selectin",
|
||||
order_by="DealStageHistory.created_at",
|
||||
)
|
||||
activities: Mapped[list["Activity"]] = relationship(
|
||||
activities: Mapped[list[Activity]] = relationship(
|
||||
"Activity", back_populates="deal", lazy="selectin"
|
||||
)
|
||||
notes: Mapped[list["Note"]] = relationship(
|
||||
notes: Mapped[list[Note]] = relationship(
|
||||
"Note",
|
||||
primaryjoin="and_(Deal.id==foreign(Note.parent_id), Note.parent_type=='deal')",
|
||||
viewonly=True,
|
||||
lazy="selectin",
|
||||
)
|
||||
tags: Mapped[list["TagLink"]] = relationship(
|
||||
tags: Mapped[list[TagLink]] = relationship(
|
||||
"TagLink",
|
||||
primaryjoin="and_(Deal.id==foreign(TagLink.parent_id), TagLink.parent_type=='deal')",
|
||||
viewonly=True,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import ForeignKey, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
@@ -11,8 +11,8 @@ from app.models.base import Base, OrgScopedMixin, TimestampMixin
|
||||
from app.models.deal import DealStage
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from app.models.user import User
|
||||
from app.models.deal import Deal
|
||||
from app.models.user import User
|
||||
|
||||
|
||||
class DealStageHistory(Base, TimestampMixin, OrgScopedMixin):
|
||||
@@ -24,16 +24,16 @@ class DealStageHistory(Base, TimestampMixin, OrgScopedMixin):
|
||||
deal_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("deals.id"), nullable=False, index=True
|
||||
)
|
||||
from_stage: Mapped[Optional[DealStage]] = mapped_column(String(32), nullable=True)
|
||||
from_stage: Mapped[DealStage | None] = mapped_column(String(32), nullable=True)
|
||||
to_stage: Mapped[DealStage] = mapped_column(String(32), nullable=False)
|
||||
changed_by: Mapped[int] = mapped_column(
|
||||
ForeignKey("users.id"), nullable=False
|
||||
)
|
||||
|
||||
deal: Mapped["Deal"] = relationship(
|
||||
deal: Mapped[Deal] = relationship(
|
||||
"Deal", back_populates="stage_history", lazy="joined"
|
||||
)
|
||||
changer: Mapped["User"] = relationship(
|
||||
changer: Mapped[User] = relationship(
|
||||
"User", foreign_keys=[changed_by], lazy="joined"
|
||||
)
|
||||
|
||||
|
||||
+1
-1
@@ -37,7 +37,7 @@ class Note(Base, TimestampMixin, SoftDeleteMixin, OrgScopedMixin):
|
||||
# Service layer (note_service.create_note) validates existence.
|
||||
parent_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True)
|
||||
|
||||
author: Mapped["User"] = relationship(
|
||||
author: Mapped[User] = relationship(
|
||||
"User", foreign_keys=[author_id], lazy="joined"
|
||||
)
|
||||
|
||||
|
||||
+3
-3
@@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import String
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
@@ -18,13 +18,13 @@ class Org(Base, TimestampMixin):
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
logo_url: Mapped[Optional[str]] = mapped_column(String(1024), nullable=True)
|
||||
logo_url: Mapped[str | None] = mapped_column(String(1024), nullable=True)
|
||||
default_currency: Mapped[str] = mapped_column(
|
||||
String(3), nullable=False, default="EUR", server_default="EUR"
|
||||
)
|
||||
|
||||
# Relationship to users (defined here to resolve circular import)
|
||||
users: Mapped[list["User"]] = relationship(
|
||||
users: Mapped[list[User]] = relationship(
|
||||
back_populates="org",
|
||||
lazy="selectin",
|
||||
cascade="all, delete-orphan",
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ class Tag(Base, TimestampMixin, SoftDeleteMixin, OrgScopedMixin):
|
||||
)
|
||||
|
||||
# Relationships
|
||||
links: Mapped[list["TagLink"]] = relationship(
|
||||
links: Mapped[list[TagLink]] = relationship(
|
||||
"TagLink", back_populates="tag", cascade="all, delete-orphan", lazy="selectin"
|
||||
)
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ class TagLink(Base, TimestampMixin, SoftDeleteMixin, OrgScopedMixin):
|
||||
# Service layer (tag_service.link_tag) validates existence.
|
||||
parent_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True)
|
||||
|
||||
tag: Mapped["Tag"] = relationship("Tag", back_populates="links", lazy="joined")
|
||||
tag: Mapped[Tag] = relationship("Tag", back_populates="links", lazy="joined")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<TagLink id={self.id} tag={self.tag_id} parent={self.parent_type}:{self.parent_id}>"
|
||||
|
||||
+3
-3
@@ -3,7 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import Boolean, String, UniqueConstraint
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
@@ -38,13 +38,13 @@ class User(Base, TimestampMixin, SoftDeleteMixin, OrgScopedMixin):
|
||||
default=UserRole.sales_rep,
|
||||
server_default=UserRole.sales_rep.value,
|
||||
)
|
||||
avatar_url: Mapped[Optional[str]] = mapped_column(String(1024), nullable=True)
|
||||
avatar_url: Mapped[str | None] = mapped_column(String(1024), nullable=True)
|
||||
email_notifications: Mapped[bool] = mapped_column(
|
||||
Boolean, nullable=False, default=True, server_default="1"
|
||||
)
|
||||
|
||||
# Relationship back to org (string reference avoids circular import at runtime)
|
||||
org: Mapped["Org"] = relationship(back_populates="users", lazy="joined")
|
||||
org: Mapped[Org] = relationship(back_populates="users", lazy="joined")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<User id={self.id} email={self.email!r} role={self.role}>"
|
||||
|
||||
+11
-12
@@ -3,8 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from typing import Any, Optional
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
@@ -15,10 +14,10 @@ class AccountBase(BaseModel):
|
||||
"""Shared fields for create/update."""
|
||||
|
||||
name: str = Field(..., min_length=1, max_length=255)
|
||||
website: Optional[str] = Field(None, max_length=512)
|
||||
industry: Optional[Industry] = None
|
||||
size: Optional[AccountSize] = None
|
||||
address: Optional[dict[str, Any]] = None
|
||||
website: str | None = Field(None, max_length=512)
|
||||
industry: Industry | None = None
|
||||
size: AccountSize | None = None
|
||||
address: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class AccountCreate(AccountBase):
|
||||
@@ -30,11 +29,11 @@ class AccountCreate(AccountBase):
|
||||
class AccountUpdate(BaseModel):
|
||||
"""Request body for PATCH /api/v1/accounts/{id}. All optional."""
|
||||
|
||||
name: Optional[str] = Field(None, min_length=1, max_length=255)
|
||||
website: Optional[str] = Field(None, max_length=512)
|
||||
industry: Optional[Industry] = None
|
||||
size: Optional[AccountSize] = None
|
||||
address: Optional[dict[str, Any]] = None
|
||||
name: str | None = Field(None, min_length=1, max_length=255)
|
||||
website: str | None = Field(None, max_length=512)
|
||||
industry: Industry | None = None
|
||||
size: AccountSize | None = None
|
||||
address: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class AccountOut(AccountBase):
|
||||
@@ -47,7 +46,7 @@ class AccountOut(AccountBase):
|
||||
owner_id: int
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
deleted_at: Optional[datetime] = None
|
||||
deleted_at: datetime | None = None
|
||||
|
||||
|
||||
class AccountListItem(AccountOut):
|
||||
|
||||
+16
-17
@@ -3,7 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
||||
|
||||
@@ -13,14 +12,14 @@ from app.models.activity import ActivityType
|
||||
class ActivityBase(BaseModel):
|
||||
type: ActivityType
|
||||
subject: str = Field(..., min_length=1, max_length=255)
|
||||
body: Optional[str] = None
|
||||
due_date: Optional[datetime] = None
|
||||
account_id: Optional[int] = None
|
||||
contact_id: Optional[int] = None
|
||||
deal_id: Optional[int] = None
|
||||
body: str | None = None
|
||||
due_date: datetime | None = None
|
||||
account_id: int | None = None
|
||||
contact_id: int | None = None
|
||||
deal_id: int | None = None
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _check_at_least_one_parent(self) -> "ActivityBase":
|
||||
def _check_at_least_one_parent(self) -> ActivityBase:
|
||||
if self.account_id is None and self.contact_id is None and self.deal_id is None:
|
||||
raise ValueError(
|
||||
"At least one of account_id, contact_id, deal_id must be set"
|
||||
@@ -33,13 +32,13 @@ class ActivityCreate(ActivityBase):
|
||||
|
||||
|
||||
class ActivityUpdate(BaseModel):
|
||||
type: Optional[ActivityType] = None
|
||||
subject: Optional[str] = Field(None, min_length=1, max_length=255)
|
||||
body: Optional[str] = None
|
||||
due_date: Optional[datetime] = None
|
||||
account_id: Optional[int] = None
|
||||
contact_id: Optional[int] = None
|
||||
deal_id: Optional[int] = None
|
||||
type: ActivityType | None = None
|
||||
subject: str | None = Field(None, min_length=1, max_length=255)
|
||||
body: str | None = None
|
||||
due_date: datetime | None = None
|
||||
account_id: int | None = None
|
||||
contact_id: int | None = None
|
||||
deal_id: int | None = None
|
||||
|
||||
|
||||
class ActivityOut(ActivityBase):
|
||||
@@ -48,16 +47,16 @@ class ActivityOut(ActivityBase):
|
||||
id: int
|
||||
org_id: int
|
||||
owner_id: int
|
||||
completed_at: Optional[datetime] = None
|
||||
completed_at: datetime | None = None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
deleted_at: Optional[datetime] = None
|
||||
deleted_at: datetime | None = None
|
||||
|
||||
|
||||
class ActivityCompleteRequest(BaseModel):
|
||||
"""Body for PATCH /api/v1/activities/{id}/complete."""
|
||||
|
||||
outcome: Optional[str] = Field(None, max_length=2048)
|
||||
outcome: str | None = Field(None, max_length=2048)
|
||||
|
||||
|
||||
__all__ = [
|
||||
|
||||
+1
-3
@@ -2,8 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, EmailStr, Field
|
||||
|
||||
from app.models.user import UserRole
|
||||
@@ -49,7 +47,7 @@ class RegisterResponse(BaseModel):
|
||||
Returns the user info (without password) plus an access token.
|
||||
"""
|
||||
|
||||
user: "UserOut"
|
||||
user: UserOut
|
||||
access_token: str
|
||||
token_type: str = "bearer"
|
||||
expires_in: int
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Generic, List, Optional, TypeVar
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
@@ -21,7 +21,7 @@ class PaginatedResponse(BaseModel, Generic[T]):
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
items: List[T] # type: ignore[valid-type]
|
||||
items: list[T] # type: ignore[valid-type]
|
||||
total: int
|
||||
skip: int
|
||||
limit: int
|
||||
|
||||
+9
-10
@@ -3,7 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, EmailStr, Field
|
||||
|
||||
@@ -11,9 +10,9 @@ from pydantic import BaseModel, ConfigDict, EmailStr, Field
|
||||
class ContactBase(BaseModel):
|
||||
first_name: str = Field(..., min_length=1, max_length=128)
|
||||
last_name: str = Field(..., min_length=1, max_length=128)
|
||||
email: Optional[EmailStr] = None
|
||||
phone: Optional[str] = Field(None, max_length=64)
|
||||
account_id: Optional[int] = None
|
||||
email: EmailStr | None = None
|
||||
phone: str | None = Field(None, max_length=64)
|
||||
account_id: int | None = None
|
||||
|
||||
|
||||
class ContactCreate(ContactBase):
|
||||
@@ -21,11 +20,11 @@ class ContactCreate(ContactBase):
|
||||
|
||||
|
||||
class ContactUpdate(BaseModel):
|
||||
first_name: Optional[str] = Field(None, min_length=1, max_length=128)
|
||||
last_name: Optional[str] = Field(None, min_length=1, max_length=128)
|
||||
email: Optional[EmailStr] = None
|
||||
phone: Optional[str] = Field(None, max_length=64)
|
||||
account_id: Optional[int] = None
|
||||
first_name: str | None = Field(None, min_length=1, max_length=128)
|
||||
last_name: str | None = Field(None, min_length=1, max_length=128)
|
||||
email: EmailStr | None = None
|
||||
phone: str | None = Field(None, max_length=64)
|
||||
account_id: int | None = None
|
||||
|
||||
|
||||
class ContactOut(ContactBase):
|
||||
@@ -36,7 +35,7 @@ class ContactOut(ContactBase):
|
||||
owner_id: int
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
deleted_at: Optional[datetime] = None
|
||||
deleted_at: datetime | None = None
|
||||
|
||||
|
||||
class ContactListItem(ContactOut):
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
@@ -27,13 +26,13 @@ class ActivityFeedItem(BaseModel):
|
||||
id: int
|
||||
type: ActivityType
|
||||
subject: str
|
||||
body: Optional[str] = None
|
||||
account_id: Optional[int] = None
|
||||
contact_id: Optional[int] = None
|
||||
deal_id: Optional[int] = None
|
||||
body: str | None = None
|
||||
account_id: int | None = None
|
||||
contact_id: int | None = None
|
||||
deal_id: int | None = None
|
||||
owner_id: int
|
||||
completed_at: Optional[datetime] = None
|
||||
due_date: Optional[datetime] = None
|
||||
completed_at: datetime | None = None
|
||||
due_date: datetime | None = None
|
||||
created_at: datetime
|
||||
|
||||
|
||||
|
||||
+9
-10
@@ -4,7 +4,6 @@ from __future__ import annotations
|
||||
|
||||
from datetime import date, datetime
|
||||
from decimal import Decimal
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
@@ -16,9 +15,9 @@ class DealBase(BaseModel):
|
||||
value: Decimal = Field(default=Decimal("0"), max_digits=12, decimal_places=2)
|
||||
currency: str = Field(default="EUR", min_length=3, max_length=3)
|
||||
stage: DealStage = DealStage.lead
|
||||
close_date: Optional[date] = None
|
||||
close_date: date | None = None
|
||||
account_id: int
|
||||
won_lost_reason: Optional[str] = Field(None, max_length=512)
|
||||
won_lost_reason: str | None = Field(None, max_length=512)
|
||||
|
||||
|
||||
class DealCreate(DealBase):
|
||||
@@ -26,11 +25,11 @@ class DealCreate(DealBase):
|
||||
|
||||
|
||||
class DealUpdate(BaseModel):
|
||||
title: Optional[str] = Field(None, min_length=1, max_length=255)
|
||||
value: Optional[Decimal] = Field(None, max_digits=12, decimal_places=2)
|
||||
currency: Optional[str] = Field(None, min_length=3, max_length=3)
|
||||
close_date: Optional[date] = None
|
||||
won_lost_reason: Optional[str] = Field(None, max_length=512)
|
||||
title: str | None = Field(None, min_length=1, max_length=255)
|
||||
value: Decimal | None = Field(None, max_digits=12, decimal_places=2)
|
||||
currency: str | None = Field(None, min_length=3, max_length=3)
|
||||
close_date: date | None = None
|
||||
won_lost_reason: str | None = Field(None, max_length=512)
|
||||
|
||||
|
||||
class DealOut(DealBase):
|
||||
@@ -41,7 +40,7 @@ class DealOut(DealBase):
|
||||
owner_id: int
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
deleted_at: Optional[datetime] = None
|
||||
deleted_at: datetime | None = None
|
||||
|
||||
|
||||
class DealListItem(DealOut):
|
||||
@@ -52,7 +51,7 @@ class DealStageUpdate(BaseModel):
|
||||
"""Body for PATCH /api/v1/deals/{id}/stage."""
|
||||
|
||||
stage: DealStage
|
||||
reason: Optional[str] = Field(None, max_length=512)
|
||||
reason: str | None = Field(None, max_length=512)
|
||||
|
||||
|
||||
class DealPipelineOut(BaseModel):
|
||||
|
||||
+2
-3
@@ -3,7 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
@@ -21,7 +20,7 @@ class NoteCreate(BaseModel):
|
||||
class NoteUpdate(BaseModel):
|
||||
"""Body for PATCH /api/v1/notes/{id}."""
|
||||
|
||||
body: Optional[str] = Field(None, min_length=1)
|
||||
body: str | None = Field(None, min_length=1)
|
||||
|
||||
|
||||
class NoteOut(BaseModel):
|
||||
@@ -37,7 +36,7 @@ class NoteOut(BaseModel):
|
||||
parent_id: int
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
deleted_at: Optional[datetime] = None
|
||||
deleted_at: datetime | None = None
|
||||
|
||||
|
||||
__all__ = ["NoteCreate", "NoteOut", "NoteUpdate"]
|
||||
|
||||
+2
-3
@@ -3,7 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
@@ -28,7 +27,7 @@ class TagOut(BaseModel):
|
||||
color: str
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
deleted_at: Optional[datetime] = None
|
||||
deleted_at: datetime | None = None
|
||||
|
||||
|
||||
class TagLinkCreate(BaseModel):
|
||||
@@ -51,7 +50,7 @@ class TagLinkOut(BaseModel):
|
||||
parent_id: int
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
deleted_at: Optional[datetime] = None
|
||||
deleted_at: datetime | None = None
|
||||
|
||||
|
||||
__all__ = ["TagCreate", "TagLinkCreate", "TagLinkOut", "TagOut"]
|
||||
|
||||
+5
-6
@@ -3,7 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, EmailStr, Field
|
||||
|
||||
@@ -20,7 +19,7 @@ class UserOut(BaseModel):
|
||||
name: str
|
||||
role: UserRole
|
||||
org_id: int
|
||||
avatar_url: Optional[str] = None
|
||||
avatar_url: str | None = None
|
||||
email_notifications: bool = True
|
||||
created_at: datetime
|
||||
|
||||
@@ -28,10 +27,10 @@ class UserOut(BaseModel):
|
||||
class UserUpdate(BaseModel):
|
||||
"""Request body for PATCH /api/v1/users/{id} (admin or self)."""
|
||||
|
||||
name: Optional[str] = Field(None, min_length=1, max_length=255)
|
||||
avatar_url: Optional[str] = Field(None, max_length=1024)
|
||||
email_notifications: Optional[bool] = None
|
||||
role: Optional[UserRole] = None # admin-only
|
||||
name: str | None = Field(None, min_length=1, max_length=255)
|
||||
avatar_url: str | None = Field(None, max_length=1024)
|
||||
email_notifications: bool | None = None
|
||||
role: UserRole | None = None # admin-only
|
||||
|
||||
|
||||
class UserCreateRequest(BaseModel):
|
||||
|
||||
@@ -11,7 +11,7 @@ router layer passes `current_user.org_id`.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Optional
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -44,7 +44,7 @@ class OrgScopedQuery:
|
||||
self,
|
||||
skip: int = 0,
|
||||
limit: int = 20,
|
||||
order_by: Optional[Any] = None,
|
||||
order_by: Any | None = None,
|
||||
**filters: Any,
|
||||
) -> list[Any]:
|
||||
"""List records scoped to org, with optional filters and pagination."""
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
@@ -19,7 +18,6 @@ async def create_account(
|
||||
db: AsyncSession, payload: AccountCreate, *, org_id: int, owner_id: int
|
||||
) -> Account:
|
||||
"""Create a new account in the given org."""
|
||||
from app.services._base import OrgScopedQuery
|
||||
|
||||
account = Account(
|
||||
org_id=org_id,
|
||||
@@ -38,7 +36,7 @@ async def create_account(
|
||||
|
||||
async def get_account(
|
||||
db: AsyncSession, account_id: int, *, org_id: int
|
||||
) -> Optional[Account]:
|
||||
) -> Account | None:
|
||||
"""Fetch a single account by id (org-scoped, not soft-deleted)."""
|
||||
from app.services._base import OrgScopedQuery
|
||||
|
||||
@@ -52,10 +50,10 @@ async def list_accounts(
|
||||
org_id: int,
|
||||
skip: int = 0,
|
||||
limit: int = 20,
|
||||
industry: Optional[str] = None,
|
||||
size: Optional[str] = None,
|
||||
owner_id: Optional[int] = None,
|
||||
q: Optional[str] = None,
|
||||
industry: str | None = None,
|
||||
size: str | None = None,
|
||||
owner_id: int | None = None,
|
||||
q: str | None = None,
|
||||
) -> list[Account]:
|
||||
"""List accounts with filters and search."""
|
||||
from app.services._base import OrgScopedQuery
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
@@ -24,9 +23,9 @@ async def _validate_parents(
|
||||
db: AsyncSession,
|
||||
*,
|
||||
org_id: int,
|
||||
account_id: Optional[int],
|
||||
contact_id: Optional[int],
|
||||
deal_id: Optional[int],
|
||||
account_id: int | None,
|
||||
contact_id: int | None,
|
||||
deal_id: int | None,
|
||||
) -> None:
|
||||
"""Ensure at least one parent is set and each provided id exists in org."""
|
||||
if account_id is None and contact_id is None and deal_id is None:
|
||||
@@ -81,7 +80,7 @@ async def create_activity(
|
||||
|
||||
async def get_activity(
|
||||
db: AsyncSession, activity_id: int, *, org_id: int
|
||||
) -> Optional[Activity]:
|
||||
) -> Activity | None:
|
||||
"""Fetch a single activity by id."""
|
||||
q = OrgScopedQuery(Activity, db, org_id=org_id)
|
||||
return await q.get(activity_id)
|
||||
@@ -93,10 +92,10 @@ async def list_activities(
|
||||
org_id: int,
|
||||
skip: int = 0,
|
||||
limit: int = 20,
|
||||
type: Optional[ActivityType] = None,
|
||||
owner_id: Optional[int] = None,
|
||||
overdue: Optional[bool] = None,
|
||||
completed: Optional[bool] = None,
|
||||
type: ActivityType | None = None,
|
||||
owner_id: int | None = None,
|
||||
overdue: bool | None = None,
|
||||
completed: bool | None = None,
|
||||
) -> list[Activity]:
|
||||
"""List activities with optional filters."""
|
||||
scoped = OrgScopedQuery(Activity, db, org_id=org_id)
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -94,7 +92,7 @@ async def register_user(
|
||||
|
||||
async def authenticate_user(
|
||||
db: AsyncSession, email: str, password: str
|
||||
) -> Optional[tuple[User, str]]:
|
||||
) -> tuple[User, str] | None:
|
||||
"""Verify credentials and return (user, token) on success, None on failure.
|
||||
|
||||
The endpoint wraps this and returns 401 on None — keeping the
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -64,7 +63,7 @@ async def create_contact(
|
||||
|
||||
async def get_contact(
|
||||
db: AsyncSession, contact_id: int, *, org_id: int
|
||||
) -> Optional[Contact]:
|
||||
) -> Contact | None:
|
||||
"""Fetch a single contact by id."""
|
||||
q = OrgScopedQuery(Contact, db, org_id=org_id)
|
||||
return await q.get(contact_id)
|
||||
@@ -76,9 +75,9 @@ async def list_contacts(
|
||||
org_id: int,
|
||||
skip: int = 0,
|
||||
limit: int = 20,
|
||||
account_id: Optional[int] = None,
|
||||
owner_id: Optional[int] = None,
|
||||
q: Optional[str] = None,
|
||||
account_id: int | None = None,
|
||||
owner_id: int | None = None,
|
||||
q: str | None = None,
|
||||
) -> list[Contact]:
|
||||
"""List contacts with filters and search."""
|
||||
scoped = OrgScopedQuery(Contact, db, org_id=org_id)
|
||||
|
||||
@@ -5,7 +5,6 @@ from __future__ import annotations
|
||||
from collections import defaultdict
|
||||
from datetime import UTC, datetime
|
||||
from decimal import Decimal
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -77,7 +76,7 @@ async def create_deal(
|
||||
|
||||
async def get_deal(
|
||||
db: AsyncSession, deal_id: int, *, org_id: int
|
||||
) -> Optional[Deal]:
|
||||
) -> Deal | None:
|
||||
"""Fetch a single deal by id."""
|
||||
q = OrgScopedQuery(Deal, db, org_id=org_id)
|
||||
return await q.get(deal_id)
|
||||
@@ -89,9 +88,9 @@ async def list_deals(
|
||||
org_id: int,
|
||||
skip: int = 0,
|
||||
limit: int = 20,
|
||||
stage: Optional[str] = None,
|
||||
owner_id: Optional[int] = None,
|
||||
account_id: Optional[int] = None,
|
||||
stage: str | None = None,
|
||||
owner_id: int | None = None,
|
||||
account_id: int | None = None,
|
||||
) -> list[Deal]:
|
||||
"""List deals with optional filters."""
|
||||
scoped = OrgScopedQuery(Deal, db, org_id=org_id)
|
||||
@@ -126,7 +125,7 @@ async def update_stage(
|
||||
new_stage: DealStage,
|
||||
*,
|
||||
changed_by: int,
|
||||
reason: Optional[str] = None,
|
||||
reason: str | None = None,
|
||||
) -> Deal:
|
||||
"""Change a deal's stage and append a DealStageHistory record."""
|
||||
from_stage_str = _stage_value(deal.stage)
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
@@ -86,7 +85,7 @@ async def create_note(
|
||||
|
||||
async def get_note(
|
||||
db: AsyncSession, note_id: int, *, org_id: int
|
||||
) -> Optional[Note]:
|
||||
) -> Note | None:
|
||||
"""Fetch a single note by id."""
|
||||
q = OrgScopedQuery(Note, db, org_id=org_id)
|
||||
return await q.get(note_id)
|
||||
@@ -98,8 +97,8 @@ async def list_notes(
|
||||
org_id: int,
|
||||
skip: int = 0,
|
||||
limit: int = 20,
|
||||
parent_type: Optional[str] = None,
|
||||
parent_id: Optional[int] = None,
|
||||
parent_type: str | None = None,
|
||||
parent_id: int | None = None,
|
||||
) -> list[Note]:
|
||||
"""List notes with optional parent filters."""
|
||||
scoped = OrgScopedQuery(Note, db, org_id=org_id)
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -75,7 +74,7 @@ async def list_tags(db: AsyncSession, *, org_id: int) -> list[Tag]:
|
||||
return await scoped.list(skip=0, limit=1000, order_by=Tag.id)
|
||||
|
||||
|
||||
async def get_tag(db: AsyncSession, tag_id: int, *, org_id: int) -> Optional[Tag]:
|
||||
async def get_tag(db: AsyncSession, tag_id: int, *, org_id: int) -> Tag | None:
|
||||
"""Fetch a single tag by id."""
|
||||
return await OrgScopedQuery(Tag, db, org_id=org_id).get(tag_id)
|
||||
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, UTC
|
||||
from typing import Optional
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -21,7 +20,7 @@ class EmailAlreadyTaken(Exception):
|
||||
"""Raised when attempting to create/update a user with an existing email."""
|
||||
|
||||
|
||||
async def get_user_by_id(db: AsyncSession, user_id: int) -> Optional[User]:
|
||||
async def get_user_by_id(db: AsyncSession, user_id: int) -> User | None:
|
||||
"""Fetch a user by ID (active only, soft-deleted excluded)."""
|
||||
result = await db.execute(
|
||||
select(User).where(User.id == user_id, User.deleted_at.is_(None))
|
||||
@@ -30,8 +29,8 @@ async def get_user_by_id(db: AsyncSession, user_id: int) -> Optional[User]:
|
||||
|
||||
|
||||
async def get_user_by_email(
|
||||
db: AsyncSession, email: str, org_id: Optional[int] = None
|
||||
) -> Optional[User]:
|
||||
db: AsyncSession, email: str, org_id: int | None = None
|
||||
) -> User | None:
|
||||
"""Fetch a user by email, optionally scoped to an org."""
|
||||
stmt = select(User).where(
|
||||
User.email == email.lower(),
|
||||
|
||||
Reference in New Issue
Block a user