ab-ms-core / app /controllers /bidders.py
PupaClic
feat(bidders): implement deletion of all barrier sizes associated with a bidder
28b4202
from fastapi import APIRouter, Depends, status, Query, HTTPException
from sqlalchemy.orm import Session
from app.db.session import get_db
from app.services.bidder_service import BidderService
from app.core.exceptions import NotFoundException
from app.schemas.bidder import BidderCreate, BidderOut
from app.schemas.paginated_response import PaginatedResponse
from app.schemas.barrier_size import (
BarrierSizesCreate,
BarrierSizesUpdate,
BarrierSizesUpdateWithAssociation,
BarrierSizesOut,
)
from app.schemas.bidders_barrier_sizes import BidderBarrierSizeDetail
from app.schemas.bidder_contact import BidderContactDetail
from app.services import barrier_size_service
from typing import Optional, List
import logging
logger = logging.getLogger(__name__)
# Common query parameter descriptions
PAGE_DESC = "Page number (1-indexed)"
PAGE_SIZE_DESC = "Number of items per page"
ORDER_BY_DESC = "Field to order by"
ORDER_DIR_DESC = "Order direction (asc|desc)"
PROJ_NO_DESC = "Project number (required)"
router = APIRouter(prefix="/api/v1/bidders", tags=["bidders"])
@router.get(
"/",
response_model=PaginatedResponse[BidderOut],
summary="List bidders by project",
response_description="Paginated list of bidders for a specific project"
)
def list_bidders(
proj_no: str = Query(..., description=PROJ_NO_DESC),
page: int = Query(1, ge=1, description=PAGE_DESC),
page_size: int = Query(10, ge=1, le=100, description=PAGE_SIZE_DESC),
order_by: Optional[str] = Query("Id", description=ORDER_BY_DESC),
order_dir: Optional[str] = Query("asc", description=ORDER_DIR_DESC),
db: Session = Depends(get_db)
):
"""Get all bidders for a specific project with pagination and ordering"""
try:
logger.info(f"Listing bidders for project {proj_no}: page={page}, page_size={page_size}")
bidder_service = BidderService(db)
result = bidder_service.get_by_project_no(
proj_no=proj_no,
page=page,
page_size=page_size,
order_by=order_by,
order_dir=order_dir.upper()
)
logger.info(f"Successfully retrieved {len(result.items)} bidders for project {proj_no}")
return result
except Exception as e:
logger.error(f"Error listing bidders for project {proj_no}: {e}")
return PaginatedResponse[BidderOut](
items=[],
page=page,
page_size=page_size,
total=0
)
@router.get(
"/project/{proj_no}",
response_model=PaginatedResponse[BidderOut],
summary="List bidders by project path parameter",
response_description="Paginated list of bidders for a specific project"
)
def list_bidders_by_project(
proj_no: str,
page: int = Query(1, ge=1, description=PAGE_DESC),
page_size: int = Query(10, ge=1, le=100, description=PAGE_SIZE_DESC),
order_by: Optional[str] = Query("Id", description=ORDER_BY_DESC),
order_dir: Optional[str] = Query("asc", description=ORDER_DIR_DESC),
db: Session = Depends(get_db)
):
"""Get all bidders for a specific project with pagination and ordering (using path parameter)"""
try:
logger.info(f"Listing bidders for project {proj_no}: page={page}, page_size={page_size}")
bidder_service = BidderService(db)
result = bidder_service.get_by_project_no(
proj_no=proj_no,
page=page,
page_size=page_size,
order_by=order_by,
order_dir=order_dir.upper()
)
logger.info(f"Successfully retrieved {len(result.items)} bidders for project {proj_no}")
return result
except Exception as e:
logger.error(f"Error listing bidders for project {proj_no}: {e}")
return PaginatedResponse[BidderOut](
items=[],
page=page,
page_size=page_size,
total=0
)
@router.get(
"/customer/{cust_id}",
response_model=PaginatedResponse[BidderOut],
summary="List bidders by customer",
response_description="Paginated list of bidders for a specific customer"
)
def list_bidders_by_customer(
cust_id: int,
page: int = Query(1, ge=1, description=PAGE_DESC),
page_size: int = Query(10, ge=1, le=100, description=PAGE_SIZE_DESC),
order_by: Optional[str] = Query("Id", description=ORDER_BY_DESC),
order_dir: Optional[str] = Query("asc", description=ORDER_DIR_DESC),
db: Session = Depends(get_db)
):
"""Get all bidders for a specific customer with pagination and ordering"""
try:
logger.info(f"Listing bidders for customer {cust_id}: page={page}, page_size={page_size}")
bidder_service = BidderService(db)
result = bidder_service.get_by_customer_id(
cust_id=cust_id,
page=page,
page_size=page_size,
order_by=order_by,
order_dir=order_dir.upper()
)
logger.info(f"Successfully retrieved {len(result.items)} bidders for customer {cust_id}")
return result
except Exception as e:
logger.error(f"Error listing bidders for customer {cust_id}: {e}")
return PaginatedResponse[BidderOut](
items=[],
page=page,
page_size=page_size,
total=0
)
@router.get(
"/all",
response_model=PaginatedResponse[BidderOut],
summary="List all bidders",
response_description="Paginated list of all bidders without any filters"
)
def list_all_bidders(
page: int = Query(1, ge=1, description=PAGE_DESC),
page_size: int = Query(10, ge=1, le=100, description=PAGE_SIZE_DESC),
order_by: Optional[str] = Query("Id", description=ORDER_BY_DESC),
order_dir: Optional[str] = Query("asc", description=ORDER_DIR_DESC),
db: Session = Depends(get_db)
):
"""Get all bidders with pagination and ordering"""
try:
logger.info(f"Listing all bidders: page={page}, page_size={page_size}")
bidder_service = BidderService(db)
result = bidder_service.list(
page=page,
page_size=page_size,
order_by=order_by,
order_direction=order_dir.upper()
)
logger.info(f"Successfully retrieved {len(result.items)} bidders")
return result
except Exception as e:
logger.error(f"Error listing all bidders: {e}")
return PaginatedResponse[BidderOut](
items=[],
page=page,
page_size=page_size,
total=0
)
# BarrierSizes endpoints - Must be defined BEFORE the generic /{bidder_id} route
@router.post(
"/barrier-sizes",
response_model=BarrierSizesOut,
status_code=status.HTTP_201_CREATED,
summary="Create a new barrier size and associate with bidder",
response_description="Created barrier size with bidder association"
)
def create_barrier_size(
obj_in: BarrierSizesCreate,
db: Session = Depends(get_db)
):
"""Create a new barrier size entry and associate it with a bidder"""
try:
logger.info(f"Creating new barrier size for bidder {obj_in.bidder_id}")
# TODO: Add bidder validation once we confirm bidder exists in database
# For now, skip validation to test the basic functionality
result = barrier_size_service.create(db, obj_in)
logger.info(f"Successfully created barrier size {result.Id} for bidder {obj_in.bidder_id}")
return result
except Exception as e:
logger.error(f"Error creating barrier size: {e}")
import traceback
traceback.print_exc() # Print full traceback for debugging
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to create barrier size: {str(e)}"
)
@router.get(
"/barrier-sizes/all",
response_model=List[BarrierSizesOut],
summary="Get all barrier sizes (for debugging)",
response_description="List of all barrier sizes in the system"
)
def get_all_barrier_sizes(db: Session = Depends(get_db)):
"""Get all barrier sizes in the system (for debugging purposes)"""
try:
barrier_sizes = barrier_size_service.get_all(db, skip=0, limit=50)
logger.info(f"Found {len(barrier_sizes)} barrier sizes in database")
return barrier_sizes
except Exception as e:
logger.error(f"Error getting all barrier sizes: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to get barrier sizes"
)
@router.get(
"/barrier-sizes/{barrier_size_id}",
response_model=BarrierSizesOut,
summary="Get a specific barrier size by ID (for debugging)",
response_description="Specific barrier size details"
)
def get_barrier_size_by_id(barrier_size_id: int, db: Session = Depends(get_db)):
"""Get a specific barrier size by ID (for debugging purposes)"""
try:
barrier_size = barrier_size_service.get(db, barrier_size_id)
if not barrier_size:
raise HTTPException(status_code=404, detail=f"Barrier size {barrier_size_id} not found")
return barrier_size
except HTTPException:
raise
except Exception as e:
logger.error(f"Error getting barrier size {barrier_size_id}: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to get barrier size"
)
@router.get(
"/{bidder_id}/barrier-sizes",
response_model=List[BidderBarrierSizeDetail],
summary="Get barrier sizes associated with a bidder",
response_description="List of barrier sizes with full details including InventoryId and InstallAdvisorFees"
)
def get_bidder_barrier_sizes(bidder_id: int, db: Session = Depends(get_db)):
"""Get all barrier sizes associated with a specific bidder"""
try:
barrier_sizes = barrier_size_service.get_by_bidder(db, bidder_id)
return barrier_sizes # Return empty list if no barrier sizes found
except Exception as e:
logger.error(f"Error getting barrier sizes for bidder {bidder_id}: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to get barrier sizes for bidder"
)
@router.get(
"/{bidder_id}/contacts",
response_model=List[BidderContactDetail],
summary="Get contacts associated with a bidder",
response_description="List of contacts associated with the bidder with full contact details"
)
def get_bidder_contacts(bidder_id: int, db: Session = Depends(get_db)):
"""Get all contacts associated with a specific bidder"""
try:
bidder_service = BidderService(db)
# First verify bidder exists
bidder = bidder_service.get(bidder_id)
# Get contacts using the repository method that already exists
contacts = bidder_service.repo.get_bidder_contacts_raw(bidder_id)
# Map to schema
contact_details = []
for contact in contacts:
contact_details.append(BidderContactDetail(
id=contact.get('Id'),
contact_id=contact.get('ContactId'),
bidder_id=contact.get('BidderId'),
enabled=contact.get('Enabled', True),
first_name=contact.get('FirstName'),
last_name=contact.get('LastName'),
title=contact.get('Title'),
email_address=contact.get('EmailAddress'),
work_phone=contact.get('WorkPhone'),
mobile_phone=contact.get('MobilePhone')
))
return contact_details
except NotFoundException:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Bidder {bidder_id} not found"
)
except Exception as e:
logger.error(f"Error getting contacts for bidder {bidder_id}: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to get contacts for bidder"
)
@router.post(
"/{bidder_id}/contacts",
response_model=List[dict],
status_code=status.HTTP_201_CREATED,
summary="Associate contacts with a bidder",
response_description="List of created BidderContact associations"
)
def add_bidder_contacts(
bidder_id: int,
contact_ids: List[int],
db: Session = Depends(get_db)
):
"""Associate multiple contacts with a bidder"""
try:
bidder_service = BidderService(db)
# First verify bidder exists
bidder = bidder_service.get(bidder_id)
# Create the associations
created_associations = bidder_service.repo.create_bidder_contacts(bidder_id, contact_ids)
logger.info(f"Successfully associated {len(contact_ids)} contacts with bidder {bidder_id}")
return created_associations
except NotFoundException:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Bidder {bidder_id} not found"
)
except Exception as e:
logger.error(f"Error associating contacts with bidder {bidder_id}: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to associate contacts with bidder"
)
@router.put(
"/barrier-sizes/{id}",
response_model=BarrierSizesOut,
summary="Update a barrier size (BarrierSizes table only)",
response_description="Updated barrier size"
)
def update_barrier_size(
id: int,
barrier_update: BarrierSizesUpdate,
db: Session = Depends(get_db)
):
"""
Update an existing barrier size in the BarrierSizes table only.
This endpoint directly updates the BarrierSizes table without checking or modifying
the BiddersBarrierSizes association table.
Only updates the fields that are provided (non-None values) in the JSON body.
Request body example:
{
"Height": 10.5,
"Width": 8.0,
"Lenght": 12.0,
"CableUnits": 5,
"Price": 1500.00,
"IsStandard": true
}
All fields in the request body are optional.
"""
try:
logger.info(f"Updating barrier size {id} (BarrierSizes table only)")
# Direct update using the barrier size service
updated_barrier = barrier_size_service.update(db, id, barrier_update)
if not updated_barrier:
raise HTTPException(
status_code=404,
detail=f"Barrier size {id} not found in BarrierSizes table"
)
logger.info(f"Successfully updated barrier size {id}")
return updated_barrier
except HTTPException:
raise
except Exception as e:
logger.error(f"Error updating barrier size {id}: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to update barrier size"
)
@router.put(
"/barrier-sizes/{id}/with-association",
response_model=dict,
summary="Update a barrier size with bidder association",
response_description="Updated barrier size and bidder association"
)
def update_barrier_size_with_association(
id: int,
barrier_update: BarrierSizesUpdateWithAssociation,
db: Session = Depends(get_db)
):
"""
Update an existing barrier size and/or its bidder association.
Updates both BarrierSizes table and BiddersBarrierSizes table if bidder_id is provided.
Only updates the fields that are provided (non-None values) in the JSON body.
Request body example:
{
"Height": 10.5,
"Width": 8.0,
"Lenght": 12.0,
"CableUnits": 5,
"Price": 1500.00,
"IsStandard": true,
"inventory_id": 123,
"install_advisor_fees": 250.00,
"bidder_id": 456
}
All fields in the request body are optional.
"""
try:
logger.info(f"Updating barrier size {id} with associations")
result = barrier_size_service.update_barrier_with_association(
db=db,
barrier_size_id=id,
height=barrier_update.Height,
width=barrier_update.Width,
length=barrier_update.Lenght,
cable_units=barrier_update.CableUnits,
price=barrier_update.Price,
is_standard=barrier_update.IsStandard,
inventory_id=barrier_update.inventory_id,
install_advisor_fees=barrier_update.install_advisor_fees,
bidder_id=barrier_update.bidder_id
)
if not result:
raise HTTPException(
status_code=404,
detail=f"Barrier size {id} not found. Please create the barrier size first using POST /api/v1/bidders/barrier-sizes"
)
logger.info(f"Successfully updated barrier size {id} with associations")
return result
except HTTPException:
raise
except Exception as e:
logger.error(f"Error updating barrier size {id} with associations: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to update barrier size with associations"
)
@router.delete(
"/barrier-sizes/by-bidder/{bidder_id}",
status_code=status.HTTP_204_NO_CONTENT,
summary="Delete all barrier sizes associated with a bidder",
response_description="All barrier sizes and associations for the bidder deleted successfully"
)
def delete_barrier_sizes_by_bidder(bidder_id: int, db: Session = Depends(get_db)):
"""
Delete all barrier sizes associated with a specific bidder.
This implements the two-step SQL logic:
1. DELETE FROM BarrierSizes WHERE Id IN (SELECT BarrierSizeId FROM BiddersBarrierSizes WHERE BidderId = {bidder_id})
2. DELETE FROM BiddersBarrierSizes WHERE BidderId = {bidder_id}
Parameters:
- bidder_id: The bidder ID to delete all barrier sizes for
"""
try:
logger.info(f"Deleting all barrier sizes for bidder {bidder_id}")
result = barrier_size_service.delete_all_by_bidder(db, bidder_id)
logger.info(f"Successfully deleted barrier sizes for bidder {bidder_id}: {result}")
return None
except Exception as e:
logger.error(f"Error deleting barrier sizes for bidder {bidder_id}: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to delete barrier sizes for bidder"
)
@router.delete(
"/barrier-sizes/bidder/{bidder_id}/barrier/{barrier_size_id}",
status_code=status.HTTP_204_NO_CONTENT,
summary="Delete barrier size by bidder and barrier size IDs",
response_description="Barrier size and association deleted successfully"
)
def delete_barrier_size_by_bidder_and_barrier(
bidder_id: int,
barrier_size_id: int,
cascade: bool = Query(True, description="If true, deletes the barrier size itself if no other bidders use it"),
db: Session = Depends(get_db)
):
"""
Delete barrier size association and optionally the barrier size itself.
This endpoint implements the SQL equivalent of:
DELETE FROM BiddersBarrierSizes WHERE BidderId={bidder_id} AND BarrierSizeId={barrier_size_id}
DELETE FROM BarrierSizes WHERE Id={barrier_size_id} (if cascade=true and no other associations exist)
Parameters:
- bidder_id: The bidder ID to remove association for
- barrier_size_id: The barrier size ID to delete association for
- cascade: If True, also deletes the BarrierSize record if no other bidders are using it
"""
try:
logger.info(f"Deleting barrier size association: bidder_id={bidder_id}, barrier_size_id={barrier_size_id}, cascade={cascade}")
result = barrier_size_service.delete_by_bidder_and_barrier_id(db, bidder_id, barrier_size_id, cascade)
if not result["association_deleted"]:
raise HTTPException(
status_code=404,
detail=f"Association not found between bidder {bidder_id} and barrier size {barrier_size_id}"
)
logger.info(f"Successfully deleted barrier size association and/or barrier size: {result}")
return None
except HTTPException:
raise
except Exception as e:
logger.error(f"Error deleting barrier size association: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to delete barrier size association"
)
# Bidder endpoints - These must come AFTER the barrier-sizes routes
@router.get(
"/{bidder_id}",
response_model=BidderOut,
summary="Get a bidder by ID",
response_description="Bidder details"
)
def get_bidder(bidder_id: int, db: Session = Depends(get_db)):
"""Get a specific bidder by ID"""
try:
bidder_service = BidderService(db)
return bidder_service.get(bidder_id)
except Exception as e:
logger.error(f"Error getting bidder {bidder_id}: {e}")
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Bidder {bidder_id} not found"
)
@router.post(
"/",
response_model=BidderOut,
status_code=status.HTTP_201_CREATED,
summary="Create a new bidder",
response_description="Created bidder details"
)
def create_bidder(
bidder_in: BidderCreate,
proj_no: str = Query(..., description=PROJ_NO_DESC),
db: Session = Depends(get_db)
):
"""
Create a new bidder with optional contact associations.
To associate multiple contacts with the bidder during creation,
include a 'contact_ids' field in the request body with a list of contact IDs.
The contact_ids field is OPTIONAL - if not provided, the bidder will be
created without any contact associations.
Example request body:
{
"cust_id": 1,
"quote": 5000.00,
"notes": "Project notes",
"email_address": "test@example.com",
"contact_ids": [1, 2, 3] // OPTIONAL: List of contact IDs to associate
}
This will create the bidder and automatically create entries in the
BidderContact table linking the bidder to the specified contacts.
If contact_ids is omitted, only the bidder record is created.
"""
try:
bidder_service = BidderService(db)
logger.info(f"Creating new bidder for project {proj_no}")
# Update the bidder data with the project number
bidder_data = bidder_in.dict()
bidder_data["proj_no"] = proj_no
result = bidder_service.create(bidder_data)
logger.info(f"Successfully created bidder {result.id} for project {proj_no}")
return result
except ValueError as e:
logger.error(f"Validation error creating bidder: {e}")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
except Exception as e:
logger.error(f"Error creating bidder: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to create bidder"
)
@router.put(
"/{bidder_id}",
response_model=BidderOut,
summary="Update a bidder",
response_description="Updated bidder details"
)
def update_bidder(
bidder_id: int,
bidder_in: BidderCreate,
proj_no: str = Query(..., description=PROJ_NO_DESC),
db: Session = Depends(get_db)
):
"""Update an existing bidder"""
try:
bidder_service = BidderService(db)
logger.info(f"Updating bidder {bidder_id} for project {proj_no}")
# Update the bidder data with the project number
bidder_data = bidder_in.dict()
bidder_data["proj_no"] = proj_no
result = bidder_service.update(bidder_id, bidder_data)
logger.info(f"Successfully updated bidder {bidder_id} for project {proj_no}")
return result
except ValueError as e:
logger.error(f"Validation error updating bidder {bidder_id}: {e}")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
except Exception as e:
logger.error(f"Error updating bidder {bidder_id}: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to update bidder"
)
@router.delete(
"/{bidder_id}",
status_code=status.HTTP_204_NO_CONTENT,
summary="Delete a bidder",
response_description="Bidder deleted successfully"
)
def delete_bidder(
bidder_id: int,
proj_no: str = Query(..., description=PROJ_NO_DESC),
db: Session = Depends(get_db)
):
"""Delete a bidder"""
try:
bidder_service = BidderService(db)
logger.info(f"Deleting bidder {bidder_id} for project {proj_no}")
# First verify the bidder belongs to the specified project
bidder = bidder_service.get(bidder_id)
if bidder.proj_no != proj_no:
raise ValueError(f"Bidder {bidder_id} does not belong to project {proj_no}")
bidder_service.delete(bidder_id)
logger.info(f"Successfully deleted bidder {bidder_id} for project {proj_no}")
return None
except Exception as e:
logger.error(f"Error deleting bidder {bidder_id}: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to delete bidder"
)