Files
maskanx_crm_backend/app/api/catalog.py
T
AFFAANhandClaude Opus 5 f6a54d6f93 Initial commit: Maskan CRM backend
Independent FastAPI backend for Maskan CRM.

Owns contacts, organizations, leads, pipelines, activities, products,
quotes, users, permissions, audit records and first-party integration
credentials, with Alembic migrations against PostgreSQL.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 10:28:41 +05:30

120 lines
4.0 KiB
Python

from fastapi import APIRouter, HTTPException, Query, status
from sqlalchemy import func, select
from sqlalchemy.exc import IntegrityError
from app.core.security import CurrentUser, Database, Writer
from app.models import Contact, Lead, Organization, Product, Quote, User
from app.schemas import Page, ProductCreate, ProductOut, QuoteCreate, QuoteOut
from app.services import add_audit, model_or_404, verify_optional_reference
router = APIRouter(tags=["Catalog and Quotes"])
@router.get("/products", response_model=Page[ProductOut])
def list_products(
user: CurrentUser,
db: Database,
page: int = Query(default=1, ge=1),
page_size: int = Query(default=25, ge=1, le=100),
) -> Page[ProductOut]:
filters = [Product.tenant_id == user.tenant_id]
total = db.scalar(select(func.count(Product.id)).where(*filters)) or 0
records = db.scalars(
select(Product)
.where(*filters)
.order_by(Product.updated_at.desc())
.offset((page - 1) * page_size)
.limit(page_size),
).all()
return Page(
items=[ProductOut.model_validate(item) for item in records],
total=total,
page=page,
page_size=page_size,
)
@router.post("/products", response_model=ProductOut, status_code=status.HTTP_201_CREATED)
def create_product(payload: ProductCreate, user: Writer, db: Database) -> ProductOut:
product = Product(tenant_id=user.tenant_id, **payload.model_dump())
db.add(product)
try:
db.flush()
add_audit(
db,
actor=user,
tenant_id=user.tenant_id,
action="product.created",
entity_type="product",
entity_id=product.id,
)
db.commit()
except IntegrityError as exc:
db.rollback()
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="A product with this SKU already exists.",
) from exc
db.refresh(product)
return ProductOut.model_validate(product)
@router.get("/quotes", response_model=Page[QuoteOut])
def list_quotes(
user: CurrentUser,
db: Database,
page: int = Query(default=1, ge=1),
page_size: int = Query(default=25, ge=1, le=100),
) -> Page[QuoteOut]:
filters = [Quote.tenant_id == user.tenant_id]
total = db.scalar(select(func.count(Quote.id)).where(*filters)) or 0
records = db.scalars(
select(Quote)
.where(*filters)
.order_by(Quote.updated_at.desc())
.offset((page - 1) * page_size)
.limit(page_size),
).all()
return Page(
items=[QuoteOut.model_validate(item) for item in records],
total=total,
page=page,
page_size=page_size,
)
@router.post("/quotes", response_model=QuoteOut, status_code=status.HTTP_201_CREATED)
def create_quote(payload: QuoteCreate, user: Writer, db: Database) -> QuoteOut:
verify_optional_reference(db, Contact, payload.contact_id, user.tenant_id)
verify_optional_reference(db, Organization, payload.organization_id, user.tenant_id)
verify_optional_reference(db, Lead, payload.lead_id, user.tenant_id)
verify_optional_reference(db, User, payload.owner_id, user.tenant_id)
quote = Quote(tenant_id=user.tenant_id, **payload.model_dump())
db.add(quote)
try:
db.flush()
add_audit(
db,
actor=user,
tenant_id=user.tenant_id,
action="quote.created",
entity_type="quote",
entity_id=quote.id,
)
db.commit()
except IntegrityError as exc:
db.rollback()
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="A quote with this number already exists.",
) from exc
db.refresh(quote)
return QuoteOut.model_validate(quote)
@router.get("/products/{product_id}", response_model=ProductOut)
def get_product(product_id: str, user: CurrentUser, db: Database) -> ProductOut:
return ProductOut.model_validate(
model_or_404(db, Product, product_id, user.tenant_id),
)