chore(quality): apply ruff autofixes and formatting (223 fixes, regression-free: 118/118 tests pass)
This commit is contained in:
@@ -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."""
|
||||
@@ -65,9 +65,13 @@ class OrgScopedQuery:
|
||||
"""Count records scoped to org, with optional filters."""
|
||||
from sqlalchemy import func as sa_func
|
||||
|
||||
stmt = select(sa_func.count()).select_from(self.model).where(
|
||||
self.model.org_id == self.org_id,
|
||||
self.model.deleted_at.is_(None),
|
||||
stmt = (
|
||||
select(sa_func.count())
|
||||
.select_from(self.model)
|
||||
.where(
|
||||
self.model.org_id == self.org_id,
|
||||
self.model.deleted_at.is_(None),
|
||||
)
|
||||
)
|
||||
for field, value in filters.items():
|
||||
if value is not None:
|
||||
|
||||
@@ -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,
|
||||
@@ -36,9 +34,7 @@ async def create_account(
|
||||
return account
|
||||
|
||||
|
||||
async def get_account(
|
||||
db: AsyncSession, account_id: int, *, org_id: int
|
||||
) -> Optional[Account]:
|
||||
async def get_account(db: AsyncSession, account_id: int, *, org_id: int) -> Account | None:
|
||||
"""Fetch a single account by id (org-scoped, not soft-deleted)."""
|
||||
from app.services._base import OrgScopedQuery
|
||||
|
||||
@@ -52,10 +48,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
|
||||
@@ -76,9 +72,7 @@ async def list_accounts(
|
||||
return base
|
||||
|
||||
|
||||
async def update_account(
|
||||
db: AsyncSession, account: Account, payload: AccountUpdate
|
||||
) -> Account:
|
||||
async def update_account(db: AsyncSession, account: Account, payload: AccountUpdate) -> Account:
|
||||
"""Apply partial updates to an account."""
|
||||
data = payload.model_dump(exclude_unset=True)
|
||||
for field, value in data.items():
|
||||
|
||||
@@ -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:
|
||||
@@ -35,14 +34,17 @@ async def _validate_parents(
|
||||
)
|
||||
if account_id is not None:
|
||||
from app.models.account import Account
|
||||
|
||||
if await OrgScopedQuery(Account, db, org_id=org_id).get(account_id) is None:
|
||||
raise NoParentException(f"Account {account_id} not found in this org")
|
||||
if contact_id is not None:
|
||||
from app.models.contact import Contact
|
||||
|
||||
if await OrgScopedQuery(Contact, db, org_id=org_id).get(contact_id) is None:
|
||||
raise NoParentException(f"Contact {contact_id} not found in this org")
|
||||
if deal_id is not None:
|
||||
from app.models.deal import Deal
|
||||
|
||||
if await OrgScopedQuery(Deal, db, org_id=org_id).get(deal_id) is None:
|
||||
raise NoParentException(f"Deal {deal_id} not found in this org")
|
||||
|
||||
@@ -79,9 +81,7 @@ async def create_activity(
|
||||
return activity
|
||||
|
||||
|
||||
async def get_activity(
|
||||
db: AsyncSession, activity_id: int, *, org_id: int
|
||||
) -> Optional[Activity]:
|
||||
async def get_activity(db: AsyncSession, activity_id: int, *, org_id: int) -> Activity | None:
|
||||
"""Fetch a single activity by id."""
|
||||
q = OrgScopedQuery(Activity, db, org_id=org_id)
|
||||
return await q.get(activity_id)
|
||||
@@ -93,10 +93,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)
|
||||
@@ -110,7 +110,8 @@ async def list_activities(
|
||||
now = datetime.now(UTC).replace(tzinfo=None) # SQLite returns naive datetimes
|
||||
if overdue is True:
|
||||
base = [
|
||||
a for a in base
|
||||
a
|
||||
for a in base
|
||||
if a.due_date is not None and a.due_date < now and a.completed_at is None
|
||||
]
|
||||
if completed is True:
|
||||
@@ -162,9 +163,7 @@ async def complete_activity(
|
||||
return activity
|
||||
|
||||
|
||||
async def soft_delete_activity(
|
||||
db: AsyncSession, activity: Activity
|
||||
) -> Activity:
|
||||
async def soft_delete_activity(db: AsyncSession, activity: Activity) -> Activity:
|
||||
"""Soft-delete an activity."""
|
||||
activity.deleted_at = datetime.now(UTC)
|
||||
await db.commit()
|
||||
|
||||
@@ -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
|
||||
@@ -35,15 +33,11 @@ class InvalidCredentials(AuthError):
|
||||
|
||||
async def count_users(db: AsyncSession) -> int:
|
||||
"""Count active (non-soft-deleted) users. Used to gate bootstrap registration."""
|
||||
result = await db.execute(
|
||||
select(User).where(User.deleted_at.is_(None))
|
||||
)
|
||||
result = await db.execute(select(User).where(User.deleted_at.is_(None)))
|
||||
return len(result.scalars().all())
|
||||
|
||||
|
||||
async def register_user(
|
||||
db: AsyncSession, payload: UserRegisterRequest
|
||||
) -> tuple[User, str]:
|
||||
async def register_user(db: AsyncSession, payload: UserRegisterRequest) -> tuple[User, str]:
|
||||
"""Bootstrap registration.
|
||||
|
||||
Creates a new Org and the first user (or a new user in the existing org
|
||||
@@ -81,9 +75,7 @@ async def register_user(
|
||||
await db.commit()
|
||||
except IntegrityError as e:
|
||||
await db.rollback()
|
||||
raise EmailAlreadyExists(
|
||||
f"A user with email {payload.email!r} already exists."
|
||||
) from e
|
||||
raise EmailAlreadyExists(f"A user with email {payload.email!r} already exists.") from e
|
||||
|
||||
await db.refresh(user)
|
||||
|
||||
@@ -92,9 +84,7 @@ async def register_user(
|
||||
return user, token
|
||||
|
||||
|
||||
async def authenticate_user(
|
||||
db: AsyncSession, email: str, password: str
|
||||
) -> Optional[tuple[User, str]]:
|
||||
async def authenticate_user(db: AsyncSession, email: str, password: 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
|
||||
@@ -22,9 +21,7 @@ class InvalidAccount(Exception):
|
||||
"""Raised when account_id points to a non-existent account."""
|
||||
|
||||
|
||||
async def _validate_account(
|
||||
db: AsyncSession, account_id: int, *, org_id: int
|
||||
) -> None:
|
||||
async def _validate_account(db: AsyncSession, account_id: int, *, org_id: int) -> None:
|
||||
"""Raise if account_id does not exist within the org."""
|
||||
if account_id is None:
|
||||
return
|
||||
@@ -62,9 +59,7 @@ async def create_contact(
|
||||
return contact
|
||||
|
||||
|
||||
async def get_contact(
|
||||
db: AsyncSession, contact_id: int, *, org_id: int
|
||||
) -> Optional[Contact]:
|
||||
async def get_contact(db: AsyncSession, contact_id: int, *, org_id: int) -> Contact | None:
|
||||
"""Fetch a single contact by id."""
|
||||
q = OrgScopedQuery(Contact, db, org_id=org_id)
|
||||
return await q.get(contact_id)
|
||||
@@ -76,9 +71,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)
|
||||
@@ -101,9 +96,7 @@ async def list_contacts(
|
||||
return base
|
||||
|
||||
|
||||
async def update_contact(
|
||||
db: AsyncSession, contact: Contact, payload: ContactUpdate
|
||||
) -> Contact:
|
||||
async def update_contact(db: AsyncSession, contact: Contact, payload: ContactUpdate) -> Contact:
|
||||
"""Apply partial updates to a contact."""
|
||||
data = payload.model_dump(exclude_unset=True)
|
||||
if "account_id" in data and data["account_id"] is not None:
|
||||
|
||||
@@ -22,16 +22,13 @@ async def get_kpis(db: AsyncSession, *, org_id: int) -> dict[str, object]:
|
||||
open_deals = [d for d in all_deals if d.stage in open_stages]
|
||||
open_deals_count = len(open_deals)
|
||||
|
||||
pipeline_value = float(
|
||||
sum((d.value for d in open_deals), Decimal("0"))
|
||||
)
|
||||
pipeline_value = float(sum((d.value for d in open_deals), Decimal("0")))
|
||||
|
||||
now = datetime.now(UTC)
|
||||
# SQLite returns naive datetimes; strip tz for comparison
|
||||
month_start = now.replace(tzinfo=None, day=1, hour=0, minute=0, second=0, microsecond=0)
|
||||
won_this_month = [
|
||||
d for d in all_deals
|
||||
if d.stage == DealStage.won and d.created_at >= month_start
|
||||
d for d in all_deals if d.stage == DealStage.won and d.created_at >= month_start
|
||||
]
|
||||
won_count = len(won_this_month)
|
||||
|
||||
@@ -50,9 +47,7 @@ async def get_kpis(db: AsyncSession, *, org_id: int) -> dict[str, object]:
|
||||
}
|
||||
|
||||
|
||||
async def get_activity_feed(
|
||||
db: AsyncSession, *, org_id: int, limit: int = 20
|
||||
) -> list[Activity]:
|
||||
async def get_activity_feed(db: AsyncSession, *, org_id: int, limit: int = 20) -> list[Activity]:
|
||||
"""Return the most recent activities, ordered by created_at desc."""
|
||||
result = await db.execute(
|
||||
select(Activity)
|
||||
|
||||
@@ -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
|
||||
@@ -75,9 +74,7 @@ async def create_deal(
|
||||
return deal
|
||||
|
||||
|
||||
async def get_deal(
|
||||
db: AsyncSession, deal_id: int, *, org_id: int
|
||||
) -> Optional[Deal]:
|
||||
async def get_deal(db: AsyncSession, deal_id: int, *, org_id: int) -> Deal | None:
|
||||
"""Fetch a single deal by id."""
|
||||
q = OrgScopedQuery(Deal, db, org_id=org_id)
|
||||
return await q.get(deal_id)
|
||||
@@ -89,9 +86,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)
|
||||
@@ -105,9 +102,7 @@ async def list_deals(
|
||||
)
|
||||
|
||||
|
||||
async def update_deal(
|
||||
db: AsyncSession, deal: Deal, payload: DealUpdate
|
||||
) -> Deal:
|
||||
async def update_deal(db: AsyncSession, deal: Deal, payload: DealUpdate) -> Deal:
|
||||
"""Apply partial updates to a deal. Does NOT change stage (use update_stage)."""
|
||||
data = payload.model_dump(exclude_unset=True)
|
||||
# Disallow direct stage changes via update endpoint
|
||||
@@ -126,7 +121,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)
|
||||
@@ -151,9 +146,7 @@ async def update_stage(
|
||||
return deal
|
||||
|
||||
|
||||
async def get_pipeline(
|
||||
db: AsyncSession, *, org_id: int
|
||||
) -> list[dict[str, object]]:
|
||||
async def get_pipeline(db: AsyncSession, *, org_id: int) -> list[dict[str, object]]:
|
||||
"""Return deals grouped by stage for the pipeline view."""
|
||||
scoped = OrgScopedQuery(Deal, db, org_id=org_id)
|
||||
all_deals = await scoped.list(skip=0, limit=10_000, order_by=Deal.id)
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
@@ -37,19 +36,13 @@ async def _validate_parent(
|
||||
"""Verify the (parent_type, parent_id) tuple points to an existing entity in org."""
|
||||
if parent_type == NoteParentType.account:
|
||||
if await OrgScopedQuery(Account, db, org_id=org_id).get(parent_id) is None:
|
||||
raise InvalidParent(
|
||||
f"Account {parent_id} not found in this org (R-2 validation)"
|
||||
)
|
||||
raise InvalidParent(f"Account {parent_id} not found in this org (R-2 validation)")
|
||||
elif parent_type == NoteParentType.contact:
|
||||
if await OrgScopedQuery(Contact, db, org_id=org_id).get(parent_id) is None:
|
||||
raise InvalidParent(
|
||||
f"Contact {parent_id} not found in this org (R-2 validation)"
|
||||
)
|
||||
raise InvalidParent(f"Contact {parent_id} not found in this org (R-2 validation)")
|
||||
elif parent_type == NoteParentType.deal:
|
||||
if await OrgScopedQuery(Deal, db, org_id=org_id).get(parent_id) is None:
|
||||
raise InvalidParent(
|
||||
f"Deal {parent_id} not found in this org (R-2 validation)"
|
||||
)
|
||||
raise InvalidParent(f"Deal {parent_id} not found in this org (R-2 validation)")
|
||||
|
||||
|
||||
async def create_note(
|
||||
@@ -84,9 +77,7 @@ async def create_note(
|
||||
return note
|
||||
|
||||
|
||||
async def get_note(
|
||||
db: AsyncSession, note_id: int, *, org_id: int
|
||||
) -> Optional[Note]:
|
||||
async def get_note(db: AsyncSession, note_id: int, *, org_id: int) -> Note | None:
|
||||
"""Fetch a single note by id."""
|
||||
q = OrgScopedQuery(Note, db, org_id=org_id)
|
||||
return await q.get(note_id)
|
||||
@@ -98,8 +89,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)
|
||||
@@ -112,9 +103,7 @@ async def list_notes(
|
||||
)
|
||||
|
||||
|
||||
async def update_note(
|
||||
db: AsyncSession, note: Note, payload: NoteUpdate
|
||||
) -> Note:
|
||||
async def update_note(db: AsyncSession, note: Note, payload: NoteUpdate) -> Note:
|
||||
"""Apply partial updates to a note. Does NOT change parent (notes are pinned)."""
|
||||
data = payload.model_dump(exclude_unset=True)
|
||||
data.pop("parent_type", None)
|
||||
|
||||
@@ -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
|
||||
@@ -39,24 +38,16 @@ async def _validate_parent(
|
||||
"""Verify (parent_type, parent_id) points to an existing entity in org."""
|
||||
if parent_type == TagLinkParentType.account:
|
||||
if await OrgScopedQuery(Account, db, org_id=org_id).get(parent_id) is None:
|
||||
raise InvalidParent(
|
||||
f"Account {parent_id} not found in this org (R-2 validation)"
|
||||
)
|
||||
raise InvalidParent(f"Account {parent_id} not found in this org (R-2 validation)")
|
||||
elif parent_type == TagLinkParentType.contact:
|
||||
if await OrgScopedQuery(Contact, db, org_id=org_id).get(parent_id) is None:
|
||||
raise InvalidParent(
|
||||
f"Contact {parent_id} not found in this org (R-2 validation)"
|
||||
)
|
||||
raise InvalidParent(f"Contact {parent_id} not found in this org (R-2 validation)")
|
||||
elif parent_type == TagLinkParentType.deal:
|
||||
if await OrgScopedQuery(Deal, db, org_id=org_id).get(parent_id) is None:
|
||||
raise InvalidParent(
|
||||
f"Deal {parent_id} not found in this org (R-2 validation)"
|
||||
)
|
||||
raise InvalidParent(f"Deal {parent_id} not found in this org (R-2 validation)")
|
||||
|
||||
|
||||
async def create_tag(
|
||||
db: AsyncSession, payload: TagCreate, *, org_id: int
|
||||
) -> Tag:
|
||||
async def create_tag(db: AsyncSession, payload: TagCreate, *, org_id: int) -> Tag:
|
||||
"""Create a new tag in the given org."""
|
||||
tag = Tag(
|
||||
org_id=org_id,
|
||||
@@ -75,14 +66,12 @@ 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)
|
||||
|
||||
|
||||
async def link_tag(
|
||||
db: AsyncSession, payload: TagLinkCreate, *, org_id: int
|
||||
) -> TagLink:
|
||||
async def link_tag(db: AsyncSession, payload: TagLinkCreate, *, org_id: int) -> TagLink:
|
||||
"""Link a tag to an entity. R-2: validates parent exists."""
|
||||
parent_type = (
|
||||
payload.parent_type
|
||||
|
||||
@@ -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,17 +20,13 @@ 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))
|
||||
)
|
||||
result = await db.execute(select(User).where(User.id == user_id, User.deleted_at.is_(None)))
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
async def get_user_by_email(
|
||||
db: AsyncSession, email: str, org_id: Optional[int] = None
|
||||
) -> Optional[User]:
|
||||
async def get_user_by_email(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(),
|
||||
@@ -73,15 +68,11 @@ async def soft_delete_user(db: AsyncSession, user: User) -> User:
|
||||
return user
|
||||
|
||||
|
||||
async def create_user_as_admin(
|
||||
db: AsyncSession, payload: UserCreateRequest, org_id: int
|
||||
) -> User:
|
||||
async def create_user_as_admin(db: AsyncSession, payload: UserCreateRequest, org_id: int) -> User:
|
||||
"""Create a new user in the given org (admin-only flow)."""
|
||||
existing = await get_user_by_email(db, payload.email, org_id=org_id)
|
||||
if existing is not None:
|
||||
raise EmailAlreadyTaken(
|
||||
f"A user with email {payload.email!r} already exists in this org."
|
||||
)
|
||||
raise EmailAlreadyTaken(f"A user with email {payload.email!r} already exists in this org.")
|
||||
|
||||
user = User(
|
||||
org_id=org_id,
|
||||
@@ -96,9 +87,7 @@ async def create_user_as_admin(
|
||||
return user
|
||||
|
||||
|
||||
async def list_users(
|
||||
db: AsyncSession, org_id: int, skip: int = 0, limit: int = 50
|
||||
) -> list[User]:
|
||||
async def list_users(db: AsyncSession, org_id: int, skip: int = 0, limit: int = 50) -> list[User]:
|
||||
"""List active users in an org, paginated."""
|
||||
result = await db.execute(
|
||||
select(User)
|
||||
|
||||
Reference in New Issue
Block a user