"""Category attributes — scoped attribute definitions per category (P2 set). Identified by composite key {category_id}/{attribute_definition_id}. The nested `attribute` object is ALWAYS included (not expand-based). """ from fastapi import APIRouter, Depends, Query from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.db.engine import get_db from app.models.catalog import AttributeDefinition, CategoryAttribute, MaterialCategory from app.schemas.catalog import CategoryAttributeCreate, CategoryAttributeUpdate from app.utils.errors import ConflictError, NotFoundError from app.utils.pagination import paginate router = APIRouter( prefix="/material-categories/{category_id}/attributes", tags=["category-attributes"], ) def ca_to_dict(ca: CategoryAttribute, attribute: AttributeDefinition) -> dict: return { "id": str(ca.id), "category_id": str(ca.category_id), "attribute_definition_id": str(ca.attribute_definition_id), "is_required": ca.is_required, "is_filterable": ca.is_filterable, "conditional_rules": ca.conditional_rules, "sort_order": ca.sort_order, "attribute": { "attribute_key": attribute.attribute_key, "label": attribute.label, "data_type": attribute.data_type, "unit_id": attribute.unit_id, "enum_values": attribute.enum_values, }, } async def _category_or_404(db: AsyncSession, category_id) -> MaterialCategory: category = await db.get(MaterialCategory, category_id) if category is None: raise NotFoundError("دسته‌بندی یافت نشد") return category async def _with_attribute(db: AsyncSession, ca: CategoryAttribute) -> dict: attribute = await db.get(AttributeDefinition, ca.attribute_definition_id) return ca_to_dict(ca, attribute) @router.get("") async def list_category_attributes( category_id, page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=100), db: AsyncSession = Depends(get_db), ): await _category_or_404(db, category_id) stmt = ( select(CategoryAttribute) .where(CategoryAttribute.category_id == category_id) .order_by(CategoryAttribute.sort_order.asc()) ) items, meta = await paginate(stmt, page, page_size, db) return {"data": [await _with_attribute(db, ca) for ca in items], "meta": meta} @router.post("", status_code=201) async def create_category_attribute( category_id, payload: CategoryAttributeCreate, db: AsyncSession = Depends(get_db) ): await _category_or_404(db, category_id) if await db.get(AttributeDefinition, payload.attribute_definition_id) is None: raise NotFoundError("تعریف ویژگی یافت نشد", field="attribute_definition_id") duplicate = await db.scalar( select(CategoryAttribute.id).where( CategoryAttribute.category_id == category_id, CategoryAttribute.attribute_definition_id == payload.attribute_definition_id, ) ) if duplicate is not None: raise ConflictError("این ویژگی قبلاً به این دسته‌بندی اضافه شده است", field="attribute_definition_id") ca = CategoryAttribute( category_id=category_id, attribute_definition_id=payload.attribute_definition_id, is_required=payload.is_required, is_filterable=payload.is_filterable, conditional_rules=payload.conditional_rules, sort_order=payload.sort_order, ) db.add(ca) await db.flush() return {"data": await _with_attribute(db, ca)} @router.patch("/{attribute_definition_id}") async def update_category_attribute( category_id, attribute_definition_id, payload: CategoryAttributeUpdate, db: AsyncSession = Depends(get_db) ): ca = await db.scalar( select(CategoryAttribute).where( CategoryAttribute.category_id == category_id, CategoryAttribute.attribute_definition_id == attribute_definition_id, ) ) if ca is None: raise NotFoundError("ویژگی این دسته‌بندی یافت نشد") if payload.is_required is not None: ca.is_required = payload.is_required if payload.is_filterable is not None: ca.is_filterable = payload.is_filterable if payload.conditional_rules is not None: ca.conditional_rules = payload.conditional_rules if payload.sort_order is not None: ca.sort_order = payload.sort_order await db.flush() return {"data": await _with_attribute(db, ca)} @router.delete("/{attribute_definition_id}") async def delete_category_attribute( category_id, attribute_definition_id, db: AsyncSession = Depends(get_db) ): ca = await db.scalar( select(CategoryAttribute).where( CategoryAttribute.category_id == category_id, CategoryAttribute.attribute_definition_id == attribute_definition_id, ) ) if ca is None: raise NotFoundError("ویژگی این دسته‌بندی یافت نشد") await db.delete(ca) await db.flush() return {"data": {"deleted": True}}