root ae72b180e5 fix: improve error handling and validation
- Add centralized exception classes
- Validate signature request status before signing
- Check contract status before approval/rejection
- Add exception handlers to FastAPI app
- Update tests for new validation logic

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 03:38:20 +08:00

209 lines
6.2 KiB
Python

"""Contract service for contract management."""
from typing import List, Optional
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.exceptions import InvalidContractStatusError, NotFoundError
from app.models.contract import Contract, ContractTemplate, ContractApproval, ContractStatus
class ContractService:
"""Service for contract operations."""
def __init__(self, db: AsyncSession):
self.db = db
async def create_contract(
self,
title: str,
party_a: str,
party_b: str,
content: str,
created_by: int,
**kwargs
) -> Contract:
"""Create a new contract."""
contract = Contract(
title=title,
party_a=party_a,
party_b=party_b,
content=content,
created_by=created_by,
**kwargs
)
self.db.add(contract)
await self.db.flush()
await self.db.refresh(contract)
return contract
async def get_contract_by_id(self, contract_id: int) -> Optional[Contract]:
"""Get a contract by ID."""
result = await self.db.execute(
select(Contract).where(Contract.id == contract_id)
)
return result.scalar_one_or_none()
async def get_contract_or_raise(self, contract_id: int) -> Contract:
"""Get a contract by ID or raise exception."""
contract = await self.get_contract_by_id(contract_id)
if not contract:
raise NotFoundError("Contract", contract_id)
return contract
async def get_contracts_list(
self,
skip: int = 0,
limit: int = 20,
status: Optional[ContractStatus] = None,
created_by: Optional[int] = None,
) -> List[Contract]:
"""Get list of contracts."""
query = select(Contract)
if status:
query = query.where(Contract.status == status)
if created_by:
query = query.where(Contract.created_by == created_by)
query = query.offset(skip).limit(limit).order_by(Contract.created_at.desc())
result = await self.db.execute(query)
return list(result.scalars().all())
async def update_contract(
self,
contract_id: int,
**kwargs
) -> Optional[Contract]:
"""Update a contract."""
contract = await self.get_contract_by_id(contract_id)
if not contract:
return None
for key, value in kwargs.items():
if hasattr(contract, key) and value is not None:
setattr(contract, key, value)
await self.db.flush()
await self.db.refresh(contract)
return contract
async def submit_for_approval(self, contract_id: int) -> Optional[Contract]:
"""Submit a contract for approval."""
contract = await self.get_contract_by_id(contract_id)
if not contract:
return None
# Only draft or rejected contracts can be submitted
if contract.status not in [ContractStatus.DRAFT, ContractStatus.REJECTED]:
raise InvalidContractStatusError(
current_status=contract.status.value,
required_status="draft or rejected"
)
return await self.update_contract(
contract_id,
status=ContractStatus.PENDING_APPROVAL
)
async def approve_contract(
self,
contract_id: int,
approver_id: int,
comment: Optional[str] = None,
) -> ContractApproval:
"""Approve a contract."""
contract = await self.get_contract_or_raise(contract_id)
# Only pending approval contracts can be approved
if contract.status != ContractStatus.PENDING_APPROVAL:
raise InvalidContractStatusError(
current_status=contract.status.value,
required_status="pending_approval"
)
approval = ContractApproval(
contract_id=contract_id,
approver_id=approver_id,
status=ContractStatus.APPROVED,
comment=comment,
)
self.db.add(approval)
# Update contract status
contract.status = ContractStatus.APPROVED
await self.db.flush()
await self.db.refresh(approval)
return approval
async def reject_contract(
self,
contract_id: int,
approver_id: int,
comment: Optional[str] = None,
) -> ContractApproval:
"""Reject a contract."""
contract = await self.get_contract_or_raise(contract_id)
# Only pending approval contracts can be rejected
if contract.status != ContractStatus.PENDING_APPROVAL:
raise InvalidContractStatusError(
current_status=contract.status.value,
required_status="pending_approval"
)
approval = ContractApproval(
contract_id=contract_id,
approver_id=approver_id,
status=ContractStatus.REJECTED,
comment=comment,
)
self.db.add(approval)
# Update contract status
contract.status = ContractStatus.REJECTED
await self.db.flush()
await self.db.refresh(approval)
return approval
class ContractTemplateService:
"""Service for contract template operations."""
def __init__(self, db: AsyncSession):
self.db = db
async def create_template(
self,
name: str,
content: str,
created_by: int,
contract_type: Optional[str] = None,
) -> ContractTemplate:
"""Create a contract template."""
template = ContractTemplate(
name=name,
content=content,
created_by=created_by,
contract_type=contract_type,
)
self.db.add(template)
await self.db.flush()
await self.db.refresh(template)
return template
async def get_templates_list(
self,
skip: int = 0,
limit: int = 20,
) -> List[ContractTemplate]:
"""Get list of templates."""
result = await self.db.execute(
select(ContractTemplate)
.offset(skip)
.limit(limit)
.order_by(ContractTemplate.created_at.desc())
)
return list(result.scalars().all())