Spaces:
Paused
Paused
| 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"]) | |
| 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 | |
| ) | |
| 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 | |
| ) | |
| 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 | |
| ) | |
| 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 | |
| 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)}" | |
| ) | |
| 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" | |
| ) | |
| 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" | |
| ) | |
| 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" | |
| ) | |
| 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" | |
| ) | |
| 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" | |
| ) | |
| 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" | |
| ) | |
| 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" | |
| ) | |
| 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" | |
| ) | |
| 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 | |
| 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" | |
| ) | |
| 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" | |
| ) | |
| 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" | |
| ) | |
| 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" | |
| ) | |