Initial Commit

This commit is contained in:
belviskhoremk
2026-05-12 00:34:21 +00:00
commit d2dc43b16f
57 changed files with 6056 additions and 0 deletions

View File

@@ -0,0 +1,116 @@
from typing import Annotated
from fastapi import APIRouter, Depends, Query, UploadFile, File
import asyncpg
from app.core.pagination import pagination_params
from app.core.responses import ok, paginated
from app.dependencies import get_db, require_admin
from app.exceptions import ValidationError
from app.models.products import ProductCreate, ProductUpdate, BulkStockUpdateRequest
from app.services import product_service, storage_service
router = APIRouter(prefix="/products", tags=["Admin — Products"])
@router.get("")
async def list_products(
pagination: Annotated[tuple, Depends(pagination_params)],
search: str | None = Query(None),
category: str | None = Query(None),
bestseller: bool = Query(False),
db: asyncpg.Connection = Depends(get_db),
_: dict = Depends(require_admin),
):
page, per_page, offset = pagination
products, total = await product_service.list_products(
db, page, per_page, offset,
include_hidden=True,
category=category,
bestseller=bestseller,
search=search,
)
return paginated(products, total, page, per_page)
@router.post("", status_code=201)
async def create_product(
body: ProductCreate,
db: asyncpg.Connection = Depends(get_db),
admin: dict = Depends(require_admin),
):
product = await product_service.create_product(db, body)
await _log(db, admin, "product.created", str(product["id"]))
return ok(product)
@router.put("/{product_id}")
async def update_product(
product_id: str,
body: ProductUpdate,
db: asyncpg.Connection = Depends(get_db),
admin: dict = Depends(require_admin),
):
product = await product_service.update_product(db, product_id, body)
await _log(db, admin, "product.updated", product_id)
return ok(product)
@router.delete("/{product_id}", status_code=204)
async def delete_product(
product_id: str,
db: asyncpg.Connection = Depends(get_db),
admin: dict = Depends(require_admin),
):
await product_service.delete_product(db, product_id)
await _log(db, admin, "product.deleted", product_id)
@router.post("/{product_id}/images", status_code=201)
async def upload_image(
product_id: str,
file: UploadFile = File(...),
db: asyncpg.Connection = Depends(get_db),
admin: dict = Depends(require_admin),
):
if not file.content_type or not file.content_type.startswith("image/"):
raise ValidationError("Only image files are allowed")
content = await file.read()
if len(content) > 10 * 1024 * 1024:
raise ValidationError("Image must be under 10MB")
image_data = await storage_service.upload_product_image(product_id, content, file.content_type)
product = await product_service.add_image(db, product_id, image_data["url"])
return ok(product)
@router.delete("/{product_id}/images")
async def delete_image(
product_id: str,
url: str = Query(..., description="Full image URL to remove"),
storage_path: str = Query(..., description="Storage path for deletion from bucket"),
db: asyncpg.Connection = Depends(get_db),
admin: dict = Depends(require_admin),
):
await storage_service.delete_product_image(storage_path)
product = await product_service.remove_image(db, product_id, url)
return ok(product)
@router.post("/bulk-stock")
async def bulk_stock(
body: BulkStockUpdateRequest,
db: asyncpg.Connection = Depends(get_db),
admin: dict = Depends(require_admin),
):
updates = [u.model_dump() for u in body.updates]
await product_service.bulk_stock_update(db, updates)
await _log(db, admin, "product.bulk_stock_updated", None)
return ok({"updated": len(updates)})
async def _log(db, admin, action, entity_id, metadata=None):
await db.execute(
"INSERT INTO activity_log (actor_id, action, entity_type, entity_id, metadata) VALUES ($1, $2, 'product', $3, $4)",
str(admin["id"]), action, entity_id, metadata,
)