Initial commit: site monorepo with API, web, and infra.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
влад
2026-07-16 10:11:54 +03:00
co-authored by Cursor
commit 016910ffb7
447 changed files with 73972 additions and 0 deletions
+151
View File
@@ -0,0 +1,151 @@
from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy import select
from app.core.database import session_scope
from app.modules.sync.tenant import TenantContext, get_tenant_context, require_enterprise_zootech
from app.modules.zootech.catalog_apply import load_catalog_row
from app.modules.zootech import service as zootech_service
from app.modules.zootech.service import ZootechServiceError
from app.modules.zootech.models import ZootechComponent, ZootechRecipe
router = APIRouter()
def _list_components(enterprise_id: str) -> list[dict]:
with session_scope() as db:
rows = list(
db.scalars(
select(ZootechComponent).where(
ZootechComponent.enterprise_id == enterprise_id,
ZootechComponent.is_deleted.is_(False),
)
)
)
return [
{
"id": row.id,
"name": row.name,
"type": row.type,
"is_active": row.is_active,
"dry_matter": row.dry_matter,
"protein": row.protein,
"energy": row.energy,
"price": row.price,
"external_no": row.external_no,
"version": row.version,
"content_hash": row.content_hash,
}
for row in rows
]
def _list_recipes(enterprise_id: str) -> list[dict]:
with session_scope() as db:
rows = list(
db.scalars(
select(ZootechRecipe).where(
ZootechRecipe.enterprise_id == enterprise_id,
ZootechRecipe.is_deleted.is_(False),
)
)
)
return [
{
"id": row.id,
"name": row.name,
"heads_per_trip": row.heads_per_trip,
"mixing_time": row.mixing_time,
"trip_percent": row.trip_percent,
"dry_matter_locked": row.dry_matter_locked,
"version": row.version,
"content_hash": row.content_hash,
}
for row in rows
]
@router.get("/components")
def list_components(
enterprise_id: str = Query(...),
tenant: TenantContext = Depends(require_enterprise_zootech),
):
if tenant.enterprise_id != enterprise_id:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="ENTERPRISE_FORBIDDEN")
return {"components": _list_components(enterprise_id)}
@router.get("/components/{component_id}")
def get_component(
component_id: str,
enterprise_id: str = Query(...),
tenant: TenantContext = Depends(require_enterprise_zootech),
):
if tenant.enterprise_id != enterprise_id:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="ENTERPRISE_FORBIDDEN")
row = load_catalog_row(enterprise_id, "component", component_id)
if not row:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="NOT_FOUND")
return row
@router.post("/components")
def create_component(
body: dict,
enterprise_id: str = Query(...),
tenant: TenantContext = Depends(require_enterprise_zootech),
):
if tenant.enterprise_id != enterprise_id:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="ENTERPRISE_FORBIDDEN")
return zootech_service.upsert_component(enterprise_id, None, body)
@router.patch("/components/{component_id}")
def update_component(
component_id: str,
body: dict,
enterprise_id: str = Query(...),
tenant: TenantContext = Depends(require_enterprise_zootech),
):
if tenant.enterprise_id != enterprise_id:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="ENTERPRISE_FORBIDDEN")
try:
return zootech_service.patch_component(enterprise_id, component_id, body)
except ZootechServiceError:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="NOT_FOUND") from None
@router.post("/recipes")
def create_recipe(
body: dict,
enterprise_id: str = Query(...),
tenant: TenantContext = Depends(require_enterprise_zootech),
):
if tenant.enterprise_id != enterprise_id:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="ENTERPRISE_FORBIDDEN")
return zootech_service.upsert_recipe(enterprise_id, None, body)
@router.get("/recipes")
def list_recipes(
enterprise_id: str = Query(...),
tenant: TenantContext = Depends(require_enterprise_zootech),
):
if tenant.enterprise_id != enterprise_id:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="ENTERPRISE_FORBIDDEN")
return {"recipes": _list_recipes(enterprise_id)}
@router.get("/recipes/{recipe_id}")
def get_recipe(
recipe_id: str,
enterprise_id: str = Query(...),
tenant: TenantContext = Depends(require_enterprise_zootech),
):
if tenant.enterprise_id != enterprise_id:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="ENTERPRISE_FORBIDDEN")
row = load_catalog_row(enterprise_id, "recipe", recipe_id)
if not row:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="NOT_FOUND")
return row