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>
120 lines
4.0 KiB
Python
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),
|
|
)
|