TorbatYar/backend/services/accounting/app/api/v1/compliance.py
Mortezakoohjani 067b499193 Connect Accounting frontend to domain APIs for phases 5.1-5.11.
Wire sales/purchase posting, settlements, compliance, payroll employees, and ops edit flows; document the integration report.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-25 22:17:41 +03:30

228 lines
6.8 KiB
Python

"""Phase 5.11 — Compliance, Audit & Governance API."""
from __future__ import annotations
from uuid import UUID
from fastapi import APIRouter, Depends, status
from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.deps import get_db, get_pagination, require_tenant
from app.core.security import get_current_user
from app.models.compliance import CompliancePolicy, GovernanceRule
from app.models.types import PolicyViolationSeverity, RiskLevel
from app.repositories.base import TenantBaseRepository
from app.services.compliance_service import AuditFramework, ComplianceEngine, GovernanceService
from shared.security import CurrentUser
router = APIRouter()
class AuditRecordCreate(BaseModel):
action: str
resource_type: str
resource_id: str
source_module: str | None = None
before_value: str | None = None
after_value: str | None = None
reason: str | None = None
class CompliancePolicyCreate(BaseModel):
code: str
name: str
policy_type: str
rules_config: str
country_code: str | None = None
class ApprovalRequestCreate(BaseModel):
workflow_id: UUID
resource_type: str
resource_id: str
class RejectRequest(BaseModel):
reason: str
class RiskCreate(BaseModel):
code: str
title: str
risk_category: str
risk_level: RiskLevel
risk_score: int = 0
description: str | None = None
class CompliancePolicyRepo(TenantBaseRepository[CompliancePolicy]):
model = CompliancePolicy
@router.get("/audit-records")
async def list_audit_records(
tenant_id: UUID = Depends(require_tenant),
pagination=Depends(get_pagination),
db: AsyncSession = Depends(get_db),
_user: CurrentUser = Depends(get_current_user),
):
from app.services.compliance_service import AuditRecordRepo
repo = AuditRecordRepo(db)
records = await repo.list_by_tenant(tenant_id, offset=pagination.offset, limit=pagination.page_size)
return [
{
"id": str(r.id),
"action": r.action,
"resource_type": r.resource_type,
"actor_user_id": r.actor_user_id,
"created_at": r.created_at.isoformat(),
}
for r in records
]
@router.post("/audit-records", status_code=status.HTTP_201_CREATED)
async def create_audit_record(
body: AuditRecordCreate,
tenant_id: UUID = Depends(require_tenant),
db: AsyncSession = Depends(get_db),
user: CurrentUser = Depends(get_current_user),
):
audit = AuditFramework(db)
record = await audit.record(
tenant_id,
actor_user_id=user.user_id,
action=body.action,
resource_type=body.resource_type,
resource_id=body.resource_id,
source_module=body.source_module,
before_value=body.before_value,
after_value=body.after_value,
reason=body.reason,
)
await db.commit()
return {"id": str(record.id)}
@router.get("/policies")
async def list_compliance_policies(
tenant_id: UUID = Depends(require_tenant),
db: AsyncSession = Depends(get_db),
_user: CurrentUser = Depends(get_current_user),
):
rows = await CompliancePolicyRepo(db).list_by_tenant(tenant_id, limit=200)
return [
{
"id": str(p.id),
"code": p.code,
"name": p.name,
"policy_type": p.policy_type,
"is_active": getattr(p, "is_active", True),
}
for p in rows
]
@router.post("/policies", status_code=status.HTTP_201_CREATED)
async def create_compliance_policy(
body: CompliancePolicyCreate,
tenant_id: UUID = Depends(require_tenant),
db: AsyncSession = Depends(get_db),
_user: CurrentUser = Depends(get_current_user),
):
repo = CompliancePolicyRepo(db)
entity = CompliancePolicy(tenant_id=tenant_id, **body.model_dump())
await repo.add(entity)
await db.commit()
return {"id": str(entity.id)}
@router.post("/approvals")
async def request_approval(
body: ApprovalRequestCreate,
tenant_id: UUID = Depends(require_tenant),
db: AsyncSession = Depends(get_db),
user: CurrentUser = Depends(get_current_user),
):
gov = GovernanceService(db)
request = await gov.request_approval(
tenant_id,
workflow_id=body.workflow_id,
resource_type=body.resource_type,
resource_id=body.resource_id,
requested_by=user.user_id,
)
await db.commit()
return {"id": str(request.id), "status": request.status.value}
@router.post("/approvals/{request_id}/approve")
async def approve_request(
request_id: UUID,
tenant_id: UUID = Depends(require_tenant),
db: AsyncSession = Depends(get_db),
user: CurrentUser = Depends(get_current_user),
):
gov = GovernanceService(db)
request = await gov.approve(tenant_id, request_id, approver_user_id=user.user_id)
await db.commit()
return {"id": str(request.id), "status": request.status.value}
@router.post("/approvals/{request_id}/reject")
async def reject_request(
request_id: UUID,
body: RejectRequest,
tenant_id: UUID = Depends(require_tenant),
db: AsyncSession = Depends(get_db),
user: CurrentUser = Depends(get_current_user),
):
gov = GovernanceService(db)
request = await gov.reject(tenant_id, request_id, approver_user_id=user.user_id, reason=body.reason)
await db.commit()
return {"id": str(request.id), "status": request.status.value}
@router.get("/risks")
async def list_risks(
tenant_id: UUID = Depends(require_tenant),
db: AsyncSession = Depends(get_db),
_user: CurrentUser = Depends(get_current_user),
):
from app.models.compliance import RiskRecord
from sqlalchemy import select
stmt = (
select(RiskRecord)
.where(RiskRecord.tenant_id == tenant_id)
.order_by(RiskRecord.created_at.desc())
.limit(200)
)
rows = list((await db.execute(stmt)).scalars().all())
return [
{
"id": str(r.id),
"code": r.code,
"title": r.title,
"risk_category": r.risk_category,
"risk_level": r.risk_level.value,
"risk_score": r.risk_score,
"is_resolved": r.is_resolved,
}
for r in rows
]
@router.post("/risks", status_code=status.HTTP_201_CREATED)
async def create_risk(
body: RiskCreate,
tenant_id: UUID = Depends(require_tenant),
db: AsyncSession = Depends(get_db),
_user: CurrentUser = Depends(get_current_user),
):
gov = GovernanceService(db)
risk = await gov.create_risk(tenant_id, **body.model_dump())
await db.commit()
return {"id": str(risk.id), "risk_level": risk.risk_level.value}