from typing import List, Dict, Any, Optional from fastapi import APIRouter, Depends, Request, Body from sqlalchemy.orm import Session from pydantic import BaseModel from adapters.db.session import get_db from adapters.api.v1.schemas import SuccessResponse from adapters.api.v1.schema_models.account import AccountTreeNode, AccountCreateRequest, AccountUpdateRequest from app.core.responses import success_response, ApiError from app.core.auth_dependency import get_current_user, AuthContext from app.core.permissions import require_business_access from adapters.db.models.account import Account from app.services.account_service import create_account, update_account, delete_account, get_account router = APIRouter(prefix="/accounts", tags=["accounts"]) class SearchAccountsRequest(BaseModel): """درخواست جستجوی حساب‌ها""" take: int = 50 skip: int = 0 search: Optional[str] = None sort_by: Optional[str] = "code" sort_desc: bool = False def _build_tree(nodes: list[Dict[str, Any]]) -> list[AccountTreeNode]: by_id: dict[int, AccountTreeNode] = {} roots: list[AccountTreeNode] = [] for n in nodes: node = AccountTreeNode( id=n['id'], code=n['code'], name=n['name'], account_type=n.get('account_type'), parent_id=n.get('parent_id') ) by_id[node.id] = node for node in list(by_id.values()): pid = node.parent_id if pid and pid in by_id: by_id[pid].children.append(node) else: roots.append(node) return roots @router.get("/business/{business_id}/tree", summary="دریافت درخت حساب‌ها برای یک کسب و کار", description="لیست حساب‌های عمومی و حساب‌های اختصاصی کسب و کار به صورت درختی", ) @require_business_access("business_id") def get_accounts_tree( request: Request, business_id: int, ctx: AuthContext = Depends(get_current_user), db: Session = Depends(get_db) ) -> dict: # دریافت حساب‌های عمومی (business_id IS NULL) و حساب‌های مختص این کسب و کار rows = db.query(Account).filter( (Account.business_id == None) | (Account.business_id == business_id) # noqa: E711 ).order_by(Account.code.asc()).all() flat = [ {"id": r.id, "code": r.code, "name": r.name, "account_type": r.account_type, "parent_id": r.parent_id} for r in rows ] tree = _build_tree(flat) return success_response({"items": [n.model_dump() for n in tree]}, request) @router.get("/business/{business_id}", summary="دریافت لیست حساب‌ها برای یک کسب و کار", description="لیست تمام حساب‌های عمومی و حساب‌های اختصاصی کسب و کار (بدون ساختار درختی)", ) @require_business_access("business_id") def get_accounts_list( request: Request, business_id: int, ctx: AuthContext = Depends(get_current_user), db: Session = Depends(get_db) ) -> dict: """دریافت لیست ساده حساب‌ها""" rows = db.query(Account).filter( (Account.business_id == None) | (Account.business_id == business_id) # noqa: E711 ).order_by(Account.code.asc()).all() items = [ { "id": r.id, "code": r.code, "name": r.name, "account_type": r.account_type, "parent_id": r.parent_id, "business_id": r.business_id, "created_at": r.created_at.isoformat() if r.created_at else None, "updated_at": r.updated_at.isoformat() if r.updated_at else None, } for r in rows ] return success_response({"items": items}, request) @router.get("/business/{business_id}/account/{account_id}", summary="دریافت جزئیات یک حساب خاص", description="دریافت اطلاعات کامل یک حساب بر اساس ID", ) @require_business_access("business_id") def get_account_by_id( request: Request, business_id: int, account_id: int, ctx: AuthContext = Depends(get_current_user), db: Session = Depends(get_db) ) -> dict: """دریافت یک حساب خاص""" account = db.query(Account).filter( Account.id == account_id, (Account.business_id == None) | (Account.business_id == business_id) # noqa: E711 ).first() if not account: from fastapi import HTTPException raise HTTPException(status_code=404, detail="حساب یافت نشد") account_data = { "id": account.id, "code": account.code, "name": account.name, "account_type": account.account_type, "parent_id": account.parent_id, "business_id": account.business_id, "created_at": account.created_at.isoformat() if account.created_at else None, "updated_at": account.updated_at.isoformat() if account.updated_at else None, } return success_response(account_data, request) @router.post("/business/{business_id}", summary="جستجو و فیلتر حساب‌ها", description="جستجو در حساب‌ها با قابلیت فیلتر، مرتب‌سازی و صفحه‌بندی", ) @require_business_access("business_id") def search_accounts( request: Request, business_id: int, search_request: SearchAccountsRequest, ctx: AuthContext = Depends(get_current_user), db: Session = Depends(get_db) ) -> dict: """جستجوی حساب‌ها با فیلتر""" query = db.query(Account).filter( (Account.business_id == None) | (Account.business_id == business_id) # noqa: E711 ) # اعمال جستجو if search_request.search: search_term = f"%{search_request.search}%" query = query.filter( (Account.code.ilike(search_term)) | (Account.name.ilike(search_term)) ) # شمارش کل total = query.count() # مرتب‌سازی if search_request.sort_by == "name": order_col = Account.name else: order_col = Account.code if search_request.sort_desc: query = query.order_by(order_col.desc()) else: query = query.order_by(order_col.asc()) # صفحه‌بندی query = query.offset(search_request.skip).limit(search_request.take) rows = query.all() items = [ { "id": r.id, "code": r.code, "name": r.name, "account_type": r.account_type, "parent_id": r.parent_id, "business_id": r.business_id, "created_at": r.created_at.isoformat() if r.created_at else None, "updated_at": r.updated_at.isoformat() if r.updated_at else None, } for r in rows ] return success_response({ "items": items, "total": total, "skip": search_request.skip, "take": search_request.take, }, request) @router.post( "/business/{business_id}/create", summary="ایجاد حساب جدید برای یک کسب‌وکار", description="ایجاد حساب اختصاصی (business-specific).", ) @require_business_access("business_id") def create_business_account( request: Request, business_id: int, body: AccountCreateRequest = Body(...), ctx: AuthContext = Depends(get_current_user), db: Session = Depends(get_db), ) -> dict: # اجازه نوشتن در بخش حسابداری لازم است if not ctx.can_write_section("accounting"): raise ApiError("FORBIDDEN", "Missing write permission for accounting", http_status=403) try: created = create_account( db, name=body.name, code=body.code, account_type=body.account_type, business_id=business_id, parent_id=body.parent_id, ) return success_response(created, request, message="ACCOUNT_CREATED") except ValueError as e: code = str(e) if code == "ACCOUNT_CODE_NOT_UNIQUE": raise ApiError("ACCOUNT_CODE_NOT_UNIQUE", "Account code must be unique per business", http_status=400) if code == "PARENT_NOT_FOUND": raise ApiError("PARENT_NOT_FOUND", "Parent account not found", http_status=400) if code == "INVALID_PARENT_BUSINESS": raise ApiError("INVALID_PARENT_BUSINESS", "Parent must be public or within the same business", http_status=400) raise @router.put( "/account/{account_id}", summary="ویرایش حساب", description="ویرایش حساب عمومی (فقط سوپرادمین) یا حساب اختصاصی بیزنس (دارای دسترسی write).", ) def update_account_endpoint( request: Request, account_id: int, body: AccountUpdateRequest = Body(...), ctx: AuthContext = Depends(get_current_user), db: Session = Depends(get_db), ) -> dict: data = get_account(db, account_id) if not data: raise ApiError("ACCOUNT_NOT_FOUND", "Account not found", http_status=404) acc_business_id = data.get("business_id") # اگر عمومی است، فقط سوپرادمین if acc_business_id is None and not ctx.is_superadmin(): raise ApiError("FORBIDDEN", "Only superadmin can edit public accounts", http_status=403) # اگر متعلق به بیزنس است باید دسترسی داشته باشد و write accounting داشته باشد if acc_business_id is not None: if not ctx.can_access_business(int(acc_business_id)): raise ApiError("FORBIDDEN", "No access to business", http_status=403) if not ctx.can_write_section("accounting"): raise ApiError("FORBIDDEN", "Missing write permission for accounting", http_status=403) try: updated = update_account( db, account_id, name=body.name, code=body.code, account_type=body.account_type, parent_id=body.parent_id, ) if updated is None: raise ApiError("ACCOUNT_NOT_FOUND", "Account not found", http_status=404) return success_response(updated, request, message="ACCOUNT_UPDATED") except ValueError as e: code = str(e) if code == "ACCOUNT_CODE_NOT_UNIQUE": raise ApiError("ACCOUNT_CODE_NOT_UNIQUE", "Account code must be unique per business", http_status=400) if code == "PARENT_NOT_FOUND": raise ApiError("PARENT_NOT_FOUND", "Parent account not found", http_status=400) if code == "INVALID_PARENT_BUSINESS": raise ApiError("INVALID_PARENT_BUSINESS", "Parent must be public or within the same business", http_status=400) raise @router.delete( "/account/{account_id}", summary="حذف حساب", description="حذف حساب عمومی (فقط سوپرادمین) یا حساب اختصاصی بیزنس (دارای دسترسی write).", ) def delete_account_endpoint( request: Request, account_id: int, ctx: AuthContext = Depends(get_current_user), db: Session = Depends(get_db), ) -> dict: data = get_account(db, account_id) if not data: raise ApiError("ACCOUNT_NOT_FOUND", "Account not found", http_status=404) acc_business_id = data.get("business_id") if acc_business_id is None and not ctx.is_superadmin(): raise ApiError("FORBIDDEN", "Only superadmin can delete public accounts", http_status=403) if acc_business_id is not None: if not ctx.can_access_business(int(acc_business_id)): raise ApiError("FORBIDDEN", "No access to business", http_status=403) if not ctx.can_write_section("accounting"): raise ApiError("FORBIDDEN", "Missing write permission for accounting", http_status=403) ok = delete_account(db, account_id) if not ok: raise ApiError("ACCOUNT_NOT_FOUND", "Account not found", http_status=404) return success_response(None, request, message="ACCOUNT_DELETED")