system-prompts-and-models-o.../salesflow-saas/backend/app/api/v1/commissions.py
2026-04-04 18:04:21 +03:00

312 lines
11 KiB
Python

from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func
from uuid import UUID
from datetime import datetime, timezone
from typing import Optional
from pydantic import BaseModel as Schema
from app.database import get_db
from app.api.deps import get_current_user, require_role
from app.models.user import User
from app.services.audit_service import record_audit
from app.services.operations_hub import emit_domain_event
from app.models.commission import Commission, CommissionStatus
router = APIRouter()
class CommissionCreate(Schema):
affiliate_id: UUID
deal_id: UUID
amount: float
rate: float
plan_type: Optional[str] = None
notes: Optional[str] = None
class CommissionUpdate(Schema):
amount: Optional[float] = None
rate: Optional[float] = None
plan_type: Optional[str] = None
notes: Optional[str] = None
class CommissionResponse(Schema):
id: UUID
tenant_id: UUID
affiliate_id: UUID
deal_id: UUID
payout_id: Optional[UUID] = None
amount: float
rate: float
plan_type: Optional[str] = None
status: str
approved_by: Optional[UUID] = None
approved_at: Optional[datetime] = None
held_reason: Optional[str] = None
paid_at: Optional[datetime] = None
payment_reference: Optional[str] = None
dispute_id: Optional[UUID] = None
notes: Optional[str] = None
created_at: datetime
model_config = {"from_attributes": True}
class CommissionListResponse(Schema):
items: list[CommissionResponse]
total: int
page: int
per_page: int
class HoldRequest(Schema):
reason: str
class ClawbackRequest(Schema):
reason: str
@router.get("", response_model=CommissionListResponse)
async def list_commissions(
affiliate_id: UUID = Query(None),
status: str = Query(None),
page: int = Query(1, ge=1),
per_page: int = Query(20, ge=1, le=100),
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
query = select(Commission).where(Commission.tenant_id == current_user.tenant_id)
if affiliate_id:
query = query.where(Commission.affiliate_id == affiliate_id)
if status:
query = query.where(Commission.status == status)
total = (await db.execute(select(func.count()).select_from(query.subquery()))).scalar()
query = query.order_by(Commission.created_at.desc()).offset((page - 1) * per_page).limit(per_page)
result = await db.execute(query)
items = [CommissionResponse.model_validate(c) for c in result.scalars().all()]
return CommissionListResponse(items=items, total=total, page=page, per_page=per_page)
@router.get("/{commission_id}", response_model=CommissionResponse)
async def get_commission(
commission_id: UUID,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(
select(Commission).where(Commission.id == commission_id, Commission.tenant_id == current_user.tenant_id)
)
commission = result.scalar_one_or_none()
if not commission:
raise HTTPException(status_code=404, detail="Commission not found")
return CommissionResponse.model_validate(commission)
@router.post("", response_model=CommissionResponse, status_code=201)
async def create_commission(
data: CommissionCreate,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
commission = Commission(
tenant_id=current_user.tenant_id,
status=CommissionStatus.DRAFT,
**data.model_dump(exclude_none=True),
)
db.add(commission)
await db.flush()
await db.refresh(commission)
return CommissionResponse.model_validate(commission)
@router.put("/{commission_id}", response_model=CommissionResponse)
async def update_commission(
commission_id: UUID,
data: CommissionUpdate,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(
select(Commission).where(Commission.id == commission_id, Commission.tenant_id == current_user.tenant_id)
)
commission = result.scalar_one_or_none()
if not commission:
raise HTTPException(status_code=404, detail="Commission not found")
if commission.status not in (CommissionStatus.DRAFT, CommissionStatus.PENDING):
raise HTTPException(status_code=400, detail="Cannot update commission in current status")
for field, value in data.model_dump(exclude_none=True).items():
setattr(commission, field, value)
await db.flush()
await db.refresh(commission)
return CommissionResponse.model_validate(commission)
@router.post("/{commission_id}/approve", response_model=CommissionResponse)
async def approve_commission(
commission_id: UUID,
current_user: User = Depends(require_role("admin", "manager")),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(
select(Commission).where(Commission.id == commission_id, Commission.tenant_id == current_user.tenant_id)
)
commission = result.scalar_one_or_none()
if not commission:
raise HTTPException(status_code=404, detail="Commission not found")
if commission.status not in (CommissionStatus.DRAFT, CommissionStatus.PENDING):
raise HTTPException(status_code=400, detail=f"Cannot approve commission with status '{commission.status.value}'")
commission.status = CommissionStatus.APPROVED
commission.approved_by = current_user.id
commission.approved_at = datetime.now(timezone.utc)
await db.flush()
await record_audit(
db,
tenant_id=current_user.tenant_id,
user_id=current_user.id,
action="commission.approve",
entity_type="commission",
entity_id=commission.id,
changes={"deal_id": str(commission.deal_id), "amount": float(commission.amount)},
)
await emit_domain_event(
db,
tenant_id=current_user.tenant_id,
event_type="commission.approved",
payload={"commission_id": str(commission.id), "deal_id": str(commission.deal_id)},
)
await db.refresh(commission)
return CommissionResponse.model_validate(commission)
@router.post("/{commission_id}/hold", response_model=CommissionResponse)
async def hold_commission(
commission_id: UUID,
data: HoldRequest,
current_user: User = Depends(require_role("admin", "manager")),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(
select(Commission).where(Commission.id == commission_id, Commission.tenant_id == current_user.tenant_id)
)
commission = result.scalar_one_or_none()
if not commission:
raise HTTPException(status_code=404, detail="Commission not found")
commission.status = CommissionStatus.HELD
commission.held_reason = data.reason
await db.flush()
await record_audit(
db,
tenant_id=current_user.tenant_id,
user_id=current_user.id,
action="commission.hold",
entity_type="commission",
entity_id=commission.id,
changes={"reason": data.reason},
)
await emit_domain_event(
db,
tenant_id=current_user.tenant_id,
event_type="commission.held",
payload={"commission_id": str(commission.id)},
)
await db.refresh(commission)
return CommissionResponse.model_validate(commission)
@router.post("/{commission_id}/pay", response_model=CommissionResponse)
async def pay_commission(
commission_id: UUID,
current_user: User = Depends(require_role("admin")),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(
select(Commission).where(Commission.id == commission_id, Commission.tenant_id == current_user.tenant_id)
)
commission = result.scalar_one_or_none()
if not commission:
raise HTTPException(status_code=404, detail="Commission not found")
if commission.status != CommissionStatus.APPROVED:
raise HTTPException(status_code=400, detail="Commission must be approved before payment")
commission.status = CommissionStatus.PAID
commission.paid_at = datetime.now(timezone.utc)
await db.flush()
await record_audit(
db,
tenant_id=current_user.tenant_id,
user_id=current_user.id,
action="commission.pay",
entity_type="commission",
entity_id=commission.id,
changes={"paid_at": commission.paid_at.isoformat() if commission.paid_at else None},
)
await emit_domain_event(
db,
tenant_id=current_user.tenant_id,
event_type="commission.paid",
payload={"commission_id": str(commission.id)},
)
await db.refresh(commission)
return CommissionResponse.model_validate(commission)
@router.post("/{commission_id}/dispute", response_model=CommissionResponse)
async def dispute_commission(
commission_id: UUID,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(
select(Commission).where(Commission.id == commission_id, Commission.tenant_id == current_user.tenant_id)
)
commission = result.scalar_one_or_none()
if not commission:
raise HTTPException(status_code=404, detail="Commission not found")
commission.status = CommissionStatus.DISPUTED
await db.flush()
await db.refresh(commission)
return CommissionResponse.model_validate(commission)
@router.post("/{commission_id}/clawback", response_model=CommissionResponse)
async def clawback_commission(
commission_id: UUID,
data: ClawbackRequest,
current_user: User = Depends(require_role("admin")),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(
select(Commission).where(Commission.id == commission_id, Commission.tenant_id == current_user.tenant_id)
)
commission = result.scalar_one_or_none()
if not commission:
raise HTTPException(status_code=404, detail="Commission not found")
if commission.status != CommissionStatus.PAID:
raise HTTPException(status_code=400, detail="Can only clawback paid commissions")
commission.status = CommissionStatus.CLAWBACK
commission.notes = f"Clawback: {data.reason}" + (f"\n{commission.notes}" if commission.notes else "")
await db.flush()
await db.refresh(commission)
return CommissionResponse.model_validate(commission)
@router.delete("/{commission_id}", status_code=204)
async def delete_commission(
commission_id: UUID,
current_user: User = Depends(require_role("admin")),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(
select(Commission).where(Commission.id == commission_id, Commission.tenant_id == current_user.tenant_id)
)
commission = result.scalar_one_or_none()
if not commission:
raise HTTPException(status_code=404, detail="Commission not found")
if commission.status not in (CommissionStatus.DRAFT,):
raise HTTPException(status_code=400, detail="Can only delete draft commissions")
await db.delete(commission)
await db.flush()