Spaces:
Paused
Paused
Commit ·
86f4f77
1
Parent(s): b799f0a
feat: Add Bidder model, repository, service, and schemas
Browse files- Implemented Bidder model with various fields in bidder.py.
- Created BidderRepository for CRUD operations and pagination in bidder_repo.py.
- Developed BidderService to handle business logic for bidders.
- Added Pydantic schemas for Bidder creation and output validation.
- Refactored ContactRepository to use direct SQLAlchemy queries instead of stored procedures.
- Updated SQL prompt for retrieving customer data.
- app/app.py +2 -0
- app/controllers/bidders.py +250 -0
- app/db/models/bidder.py +29 -0
- app/db/repositories/bidder_repo.py +274 -0
- app/db/repositories/contact_repo.py +112 -272
- app/prompts/SELECT TOP (1000) [CustomerID].sql +2 -19
- app/schemas/bidder.py +55 -0
- app/services/bidder_service.py +96 -0
app/app.py
CHANGED
|
@@ -10,6 +10,7 @@ from app.controllers.dashboard import router as dashboard_router
|
|
| 10 |
from app.controllers.reports import router as reports_router
|
| 11 |
from app.controllers.employees import router as employees_router
|
| 12 |
from app.controllers.reference import router as reference_router
|
|
|
|
| 13 |
|
| 14 |
setup_logging(settings.LOG_LEVEL)
|
| 15 |
|
|
@@ -31,3 +32,4 @@ app.include_router(dashboard_router)
|
|
| 31 |
app.include_router(reports_router)
|
| 32 |
app.include_router(employees_router)
|
| 33 |
app.include_router(reference_router)
|
|
|
|
|
|
| 10 |
from app.controllers.reports import router as reports_router
|
| 11 |
from app.controllers.employees import router as employees_router
|
| 12 |
from app.controllers.reference import router as reference_router
|
| 13 |
+
from app.controllers.bidders import router as bidders_router
|
| 14 |
|
| 15 |
setup_logging(settings.LOG_LEVEL)
|
| 16 |
|
|
|
|
| 32 |
app.include_router(reports_router)
|
| 33 |
app.include_router(employees_router)
|
| 34 |
app.include_router(reference_router)
|
| 35 |
+
app.include_router(bidders_router)
|
app/controllers/bidders.py
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, Depends, status, Query, HTTPException
|
| 2 |
+
from sqlalchemy.orm import Session
|
| 3 |
+
from app.db.session import get_db
|
| 4 |
+
from app.services.bidder_service import BidderService
|
| 5 |
+
from app.schemas.bidder import BidderCreate, BidderOut
|
| 6 |
+
from app.schemas.paginated_response import PaginatedResponse
|
| 7 |
+
from typing import Optional
|
| 8 |
+
import logging
|
| 9 |
+
|
| 10 |
+
logger = logging.getLogger(__name__)
|
| 11 |
+
|
| 12 |
+
# Common query parameter descriptions
|
| 13 |
+
PAGE_DESC = "Page number (1-indexed)"
|
| 14 |
+
PAGE_SIZE_DESC = "Number of items per page"
|
| 15 |
+
ORDER_BY_DESC = "Field to order by"
|
| 16 |
+
ORDER_DIR_DESC = "Order direction (asc|desc)"
|
| 17 |
+
PROJ_NO_DESC = "Project number (required)"
|
| 18 |
+
|
| 19 |
+
router = APIRouter(prefix="/api/v1/bidders", tags=["bidders"])
|
| 20 |
+
|
| 21 |
+
@router.get(
|
| 22 |
+
"/",
|
| 23 |
+
response_model=PaginatedResponse[BidderOut],
|
| 24 |
+
summary="List bidders by project",
|
| 25 |
+
response_description="Paginated list of bidders for a specific project"
|
| 26 |
+
)
|
| 27 |
+
def list_bidders(
|
| 28 |
+
proj_no: str = Query(..., description=PROJ_NO_DESC),
|
| 29 |
+
page: int = Query(1, ge=1, description=PAGE_DESC),
|
| 30 |
+
page_size: int = Query(10, ge=1, le=100, description=PAGE_SIZE_DESC),
|
| 31 |
+
order_by: Optional[str] = Query("Id", description=ORDER_BY_DESC),
|
| 32 |
+
order_dir: Optional[str] = Query("asc", description=ORDER_DIR_DESC),
|
| 33 |
+
db: Session = Depends(get_db)
|
| 34 |
+
):
|
| 35 |
+
"""Get all bidders for a specific project with pagination and ordering"""
|
| 36 |
+
try:
|
| 37 |
+
logger.info(f"Listing bidders for project {proj_no}: page={page}, page_size={page_size}")
|
| 38 |
+
bidder_service = BidderService(db)
|
| 39 |
+
result = bidder_service.get_by_project_no(
|
| 40 |
+
proj_no=proj_no,
|
| 41 |
+
page=page,
|
| 42 |
+
page_size=page_size,
|
| 43 |
+
order_by=order_by,
|
| 44 |
+
order_dir=order_dir.upper()
|
| 45 |
+
)
|
| 46 |
+
logger.info(f"Successfully retrieved {len(result.items)} bidders for project {proj_no}")
|
| 47 |
+
return result
|
| 48 |
+
except Exception as e:
|
| 49 |
+
logger.error(f"Error listing bidders for project {proj_no}: {e}")
|
| 50 |
+
return PaginatedResponse[BidderOut](
|
| 51 |
+
items=[],
|
| 52 |
+
page=page,
|
| 53 |
+
page_size=page_size,
|
| 54 |
+
total=0
|
| 55 |
+
)
|
| 56 |
+
|
| 57 |
+
@router.get(
|
| 58 |
+
"/project/{proj_no}",
|
| 59 |
+
response_model=PaginatedResponse[BidderOut],
|
| 60 |
+
summary="List bidders by project path parameter",
|
| 61 |
+
response_description="Paginated list of bidders for a specific project"
|
| 62 |
+
)
|
| 63 |
+
def list_bidders_by_project(
|
| 64 |
+
proj_no: str,
|
| 65 |
+
page: int = Query(1, ge=1, description=PAGE_DESC),
|
| 66 |
+
page_size: int = Query(10, ge=1, le=100, description=PAGE_SIZE_DESC),
|
| 67 |
+
order_by: Optional[str] = Query("Id", description=ORDER_BY_DESC),
|
| 68 |
+
order_dir: Optional[str] = Query("asc", description=ORDER_DIR_DESC),
|
| 69 |
+
db: Session = Depends(get_db)
|
| 70 |
+
):
|
| 71 |
+
"""Get all bidders for a specific project with pagination and ordering (using path parameter)"""
|
| 72 |
+
try:
|
| 73 |
+
logger.info(f"Listing bidders for project {proj_no}: page={page}, page_size={page_size}")
|
| 74 |
+
bidder_service = BidderService(db)
|
| 75 |
+
result = bidder_service.get_by_project_no(
|
| 76 |
+
proj_no=proj_no,
|
| 77 |
+
page=page,
|
| 78 |
+
page_size=page_size,
|
| 79 |
+
order_by=order_by,
|
| 80 |
+
order_dir=order_dir.upper()
|
| 81 |
+
)
|
| 82 |
+
logger.info(f"Successfully retrieved {len(result.items)} bidders for project {proj_no}")
|
| 83 |
+
return result
|
| 84 |
+
except Exception as e:
|
| 85 |
+
logger.error(f"Error listing bidders for project {proj_no}: {e}")
|
| 86 |
+
return PaginatedResponse[BidderOut](
|
| 87 |
+
items=[],
|
| 88 |
+
page=page,
|
| 89 |
+
page_size=page_size,
|
| 90 |
+
total=0
|
| 91 |
+
)
|
| 92 |
+
|
| 93 |
+
@router.get(
|
| 94 |
+
"/customer/{cust_id}",
|
| 95 |
+
response_model=PaginatedResponse[BidderOut],
|
| 96 |
+
summary="List bidders by customer",
|
| 97 |
+
response_description="Paginated list of bidders for a specific customer"
|
| 98 |
+
)
|
| 99 |
+
def list_bidders_by_customer(
|
| 100 |
+
cust_id: int,
|
| 101 |
+
page: int = Query(1, ge=1, description=PAGE_DESC),
|
| 102 |
+
page_size: int = Query(10, ge=1, le=100, description=PAGE_SIZE_DESC),
|
| 103 |
+
order_by: Optional[str] = Query("Id", description=ORDER_BY_DESC),
|
| 104 |
+
order_dir: Optional[str] = Query("asc", description=ORDER_DIR_DESC),
|
| 105 |
+
db: Session = Depends(get_db)
|
| 106 |
+
):
|
| 107 |
+
"""Get all bidders for a specific customer with pagination and ordering"""
|
| 108 |
+
try:
|
| 109 |
+
logger.info(f"Listing bidders for customer {cust_id}: page={page}, page_size={page_size}")
|
| 110 |
+
bidder_service = BidderService(db)
|
| 111 |
+
result = bidder_service.get_by_customer_id(
|
| 112 |
+
cust_id=cust_id,
|
| 113 |
+
page=page,
|
| 114 |
+
page_size=page_size,
|
| 115 |
+
order_by=order_by,
|
| 116 |
+
order_dir=order_dir.upper()
|
| 117 |
+
)
|
| 118 |
+
logger.info(f"Successfully retrieved {len(result.items)} bidders for customer {cust_id}")
|
| 119 |
+
return result
|
| 120 |
+
except Exception as e:
|
| 121 |
+
logger.error(f"Error listing bidders for customer {cust_id}: {e}")
|
| 122 |
+
return PaginatedResponse[BidderOut](
|
| 123 |
+
items=[],
|
| 124 |
+
page=page,
|
| 125 |
+
page_size=page_size,
|
| 126 |
+
total=0
|
| 127 |
+
)
|
| 128 |
+
|
| 129 |
+
@router.get(
|
| 130 |
+
"/{bidder_id}",
|
| 131 |
+
response_model=BidderOut,
|
| 132 |
+
summary="Get a bidder by ID",
|
| 133 |
+
response_description="Bidder details"
|
| 134 |
+
)
|
| 135 |
+
def get_bidder(bidder_id: int, db: Session = Depends(get_db)):
|
| 136 |
+
"""Get a specific bidder by ID"""
|
| 137 |
+
try:
|
| 138 |
+
bidder_service = BidderService(db)
|
| 139 |
+
return bidder_service.get(bidder_id)
|
| 140 |
+
except Exception as e:
|
| 141 |
+
logger.error(f"Error getting bidder {bidder_id}: {e}")
|
| 142 |
+
raise HTTPException(
|
| 143 |
+
status_code=status.HTTP_404_NOT_FOUND,
|
| 144 |
+
detail=f"Bidder {bidder_id} not found"
|
| 145 |
+
)
|
| 146 |
+
|
| 147 |
+
@router.post(
|
| 148 |
+
"/",
|
| 149 |
+
response_model=BidderOut,
|
| 150 |
+
status_code=status.HTTP_201_CREATED,
|
| 151 |
+
summary="Create a new bidder",
|
| 152 |
+
response_description="Created bidder details"
|
| 153 |
+
)
|
| 154 |
+
def create_bidder(
|
| 155 |
+
bidder_in: BidderCreate,
|
| 156 |
+
proj_no: str = Query(..., description=PROJ_NO_DESC),
|
| 157 |
+
db: Session = Depends(get_db)
|
| 158 |
+
):
|
| 159 |
+
"""Create a new bidder"""
|
| 160 |
+
try:
|
| 161 |
+
bidder_service = BidderService(db)
|
| 162 |
+
logger.info(f"Creating new bidder for project {proj_no}")
|
| 163 |
+
|
| 164 |
+
# Update the bidder data with the project number
|
| 165 |
+
bidder_data = bidder_in.dict()
|
| 166 |
+
bidder_data["proj_no"] = proj_no
|
| 167 |
+
|
| 168 |
+
result = bidder_service.create(bidder_data)
|
| 169 |
+
logger.info(f"Successfully created bidder {result.id} for project {proj_no}")
|
| 170 |
+
return result
|
| 171 |
+
except ValueError as e:
|
| 172 |
+
logger.error(f"Validation error creating bidder: {e}")
|
| 173 |
+
raise HTTPException(
|
| 174 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 175 |
+
detail=str(e)
|
| 176 |
+
)
|
| 177 |
+
except Exception as e:
|
| 178 |
+
logger.error(f"Error creating bidder: {e}")
|
| 179 |
+
raise HTTPException(
|
| 180 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 181 |
+
detail="Failed to create bidder"
|
| 182 |
+
)
|
| 183 |
+
|
| 184 |
+
@router.put(
|
| 185 |
+
"/{bidder_id}",
|
| 186 |
+
response_model=BidderOut,
|
| 187 |
+
summary="Update a bidder",
|
| 188 |
+
response_description="Updated bidder details"
|
| 189 |
+
)
|
| 190 |
+
def update_bidder(
|
| 191 |
+
bidder_id: int,
|
| 192 |
+
bidder_in: BidderCreate,
|
| 193 |
+
proj_no: str = Query(..., description=PROJ_NO_DESC),
|
| 194 |
+
db: Session = Depends(get_db)
|
| 195 |
+
):
|
| 196 |
+
"""Update an existing bidder"""
|
| 197 |
+
try:
|
| 198 |
+
bidder_service = BidderService(db)
|
| 199 |
+
logger.info(f"Updating bidder {bidder_id} for project {proj_no}")
|
| 200 |
+
|
| 201 |
+
# Update the bidder data with the project number
|
| 202 |
+
bidder_data = bidder_in.dict()
|
| 203 |
+
bidder_data["proj_no"] = proj_no
|
| 204 |
+
|
| 205 |
+
result = bidder_service.update(bidder_id, bidder_data)
|
| 206 |
+
logger.info(f"Successfully updated bidder {bidder_id} for project {proj_no}")
|
| 207 |
+
return result
|
| 208 |
+
except ValueError as e:
|
| 209 |
+
logger.error(f"Validation error updating bidder {bidder_id}: {e}")
|
| 210 |
+
raise HTTPException(
|
| 211 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 212 |
+
detail=str(e)
|
| 213 |
+
)
|
| 214 |
+
except Exception as e:
|
| 215 |
+
logger.error(f"Error updating bidder {bidder_id}: {e}")
|
| 216 |
+
raise HTTPException(
|
| 217 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 218 |
+
detail="Failed to update bidder"
|
| 219 |
+
)
|
| 220 |
+
|
| 221 |
+
@router.delete(
|
| 222 |
+
"/{bidder_id}",
|
| 223 |
+
status_code=status.HTTP_204_NO_CONTENT,
|
| 224 |
+
summary="Delete a bidder",
|
| 225 |
+
response_description="Bidder deleted successfully"
|
| 226 |
+
)
|
| 227 |
+
def delete_bidder(
|
| 228 |
+
bidder_id: int,
|
| 229 |
+
proj_no: str = Query(..., description=PROJ_NO_DESC),
|
| 230 |
+
db: Session = Depends(get_db)
|
| 231 |
+
):
|
| 232 |
+
"""Delete a bidder"""
|
| 233 |
+
try:
|
| 234 |
+
bidder_service = BidderService(db)
|
| 235 |
+
logger.info(f"Deleting bidder {bidder_id} for project {proj_no}")
|
| 236 |
+
|
| 237 |
+
# First verify the bidder belongs to the specified project
|
| 238 |
+
bidder = bidder_service.get(bidder_id)
|
| 239 |
+
if bidder.proj_no != proj_no:
|
| 240 |
+
raise ValueError(f"Bidder {bidder_id} does not belong to project {proj_no}")
|
| 241 |
+
|
| 242 |
+
bidder_service.delete(bidder_id)
|
| 243 |
+
logger.info(f"Successfully deleted bidder {bidder_id} for project {proj_no}")
|
| 244 |
+
return None
|
| 245 |
+
except Exception as e:
|
| 246 |
+
logger.error(f"Error deleting bidder {bidder_id}: {e}")
|
| 247 |
+
raise HTTPException(
|
| 248 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 249 |
+
detail="Failed to delete bidder"
|
| 250 |
+
)
|
app/db/models/bidder.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sqlalchemy import Column, Integer, String, Boolean, Float, DateTime, ForeignKey
|
| 2 |
+
from app.db.base import Base
|
| 3 |
+
|
| 4 |
+
class Bidder(Base):
|
| 5 |
+
__tablename__ = "Bidders"
|
| 6 |
+
|
| 7 |
+
Id = Column(Integer, primary_key=True, index=True)
|
| 8 |
+
ProjNo = Column(String(50), nullable=True)
|
| 9 |
+
CustId = Column(Integer, nullable=True)
|
| 10 |
+
Quote = Column(Float, nullable=True)
|
| 11 |
+
Contact = Column(String(200), nullable=True)
|
| 12 |
+
Phone = Column(String(50), nullable=True)
|
| 13 |
+
Notes = Column(String(500), nullable=True)
|
| 14 |
+
DateLastContact = Column(DateTime, nullable=True)
|
| 15 |
+
DateFollowup = Column(DateTime, nullable=True)
|
| 16 |
+
Primary = Column(Boolean, nullable=True, default=False)
|
| 17 |
+
CustType = Column(String(50), nullable=True)
|
| 18 |
+
EmailAddress = Column(String(255), nullable=True)
|
| 19 |
+
Fax = Column(String(50), nullable=True)
|
| 20 |
+
OrderNr = Column(String(50), nullable=True)
|
| 21 |
+
CustomerPO = Column(String(50), nullable=True)
|
| 22 |
+
ShipDate = Column(DateTime, nullable=True)
|
| 23 |
+
DeliverDate = Column(DateTime, nullable=True)
|
| 24 |
+
ReplacementCost = Column(Float, nullable=True)
|
| 25 |
+
QuoteDate = Column(DateTime, nullable=True)
|
| 26 |
+
InvoiceDate = Column(DateTime, nullable=True)
|
| 27 |
+
LessPayment = Column(Float, nullable=True)
|
| 28 |
+
Enabled = Column(Boolean, nullable=False, default=True)
|
| 29 |
+
EmployeeId = Column(Integer, nullable=True)
|
app/db/repositories/bidder_repo.py
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sqlalchemy.orm import Session
|
| 2 |
+
from sqlalchemy.exc import SQLAlchemyError
|
| 3 |
+
from app.db.models.bidder import Bidder
|
| 4 |
+
from app.schemas.bidder import BidderCreate, BidderOut
|
| 5 |
+
from app.schemas.paginated_response import PaginatedResponse
|
| 6 |
+
from app.core.exceptions import NotFoundException
|
| 7 |
+
from typing import List, Optional
|
| 8 |
+
import logging
|
| 9 |
+
|
| 10 |
+
logger = logging.getLogger(__name__)
|
| 11 |
+
|
| 12 |
+
class BidderRepository:
|
| 13 |
+
def __init__(self, db: Session):
|
| 14 |
+
self.db = db
|
| 15 |
+
|
| 16 |
+
def get(self, bidder_id: int) -> Optional[BidderOut]:
|
| 17 |
+
"""Get a single bidder by ID using Bidders table directly."""
|
| 18 |
+
try:
|
| 19 |
+
bidder = self.db.query(Bidder).filter(Bidder.Id == bidder_id).first()
|
| 20 |
+
if bidder:
|
| 21 |
+
return self._map_bidder_data(bidder.__dict__)
|
| 22 |
+
return None
|
| 23 |
+
except SQLAlchemyError as e:
|
| 24 |
+
logger.error(f"Database error getting bidder {bidder_id}: {e}")
|
| 25 |
+
raise
|
| 26 |
+
except Exception as e:
|
| 27 |
+
logger.error(f"Unexpected error getting bidder {bidder_id}: {e}")
|
| 28 |
+
raise
|
| 29 |
+
|
| 30 |
+
def list(self, page: int = 1, page_size: int = 10, order_by: str = "Id", order_direction: str = "ASC") -> PaginatedResponse[BidderOut]:
|
| 31 |
+
"""Get paginated list of all bidders using Bidders table directly with order by."""
|
| 32 |
+
try:
|
| 33 |
+
query = self.db.query(Bidder)
|
| 34 |
+
# Validate order_by field
|
| 35 |
+
if not hasattr(Bidder, order_by):
|
| 36 |
+
order_by = "Id"
|
| 37 |
+
order_col = getattr(Bidder, order_by)
|
| 38 |
+
if order_direction.upper() == "DESC":
|
| 39 |
+
order_col = order_col.desc()
|
| 40 |
+
else:
|
| 41 |
+
order_col = order_col.asc()
|
| 42 |
+
query = query.order_by(order_col)
|
| 43 |
+
total_records = query.count()
|
| 44 |
+
bidders = query.offset((page - 1) * page_size).limit(page_size).all()
|
| 45 |
+
bidder_out_list = [self._map_bidder_data(bidder.__dict__) for bidder in bidders]
|
| 46 |
+
return PaginatedResponse[BidderOut](
|
| 47 |
+
items=bidder_out_list,
|
| 48 |
+
page=page,
|
| 49 |
+
page_size=page_size,
|
| 50 |
+
total=total_records
|
| 51 |
+
)
|
| 52 |
+
except Exception as e:
|
| 53 |
+
logger.error(f"Error listing bidders: {e}")
|
| 54 |
+
return PaginatedResponse[BidderOut](
|
| 55 |
+
items=[],
|
| 56 |
+
page=page,
|
| 57 |
+
page_size=page_size,
|
| 58 |
+
total=0
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
def get_by_project_no(self, proj_no: str, page: int = 1, page_size: int = 10,
|
| 62 |
+
order_by: str = "Id", order_dir: str = "ASC") -> PaginatedResponse[BidderOut]:
|
| 63 |
+
"""Get bidders for a specific project using Bidders table directly."""
|
| 64 |
+
try:
|
| 65 |
+
# Handle string or integer proj_no values
|
| 66 |
+
# Try both the original value and converted value to handle both cases
|
| 67 |
+
original_proj_no = proj_no
|
| 68 |
+
|
| 69 |
+
# Try to handle numeric project numbers stored in database as int or string
|
| 70 |
+
try:
|
| 71 |
+
numeric_proj_no = int(proj_no)
|
| 72 |
+
query = self.db.query(Bidder).filter(
|
| 73 |
+
(Bidder.ProjNo == original_proj_no) |
|
| 74 |
+
(Bidder.ProjNo == str(numeric_proj_no)) |
|
| 75 |
+
(Bidder.ProjNo == numeric_proj_no)
|
| 76 |
+
)
|
| 77 |
+
except (ValueError, TypeError):
|
| 78 |
+
# If conversion to int fails, just use the original string
|
| 79 |
+
query = self.db.query(Bidder).filter(Bidder.ProjNo == original_proj_no)
|
| 80 |
+
# Validate order_by field
|
| 81 |
+
if not hasattr(Bidder, order_by):
|
| 82 |
+
order_by = "Id"
|
| 83 |
+
order_col = getattr(Bidder, order_by)
|
| 84 |
+
if order_dir.upper() == "DESC":
|
| 85 |
+
order_col = order_col.desc()
|
| 86 |
+
else:
|
| 87 |
+
order_col = order_col.asc()
|
| 88 |
+
query = query.order_by(order_col)
|
| 89 |
+
total_records = query.count()
|
| 90 |
+
bidders = query.offset((page - 1) * page_size).limit(page_size).all()
|
| 91 |
+
bidder_out_list = [self._map_bidder_data(bidder.__dict__) for bidder in bidders]
|
| 92 |
+
return PaginatedResponse[BidderOut](
|
| 93 |
+
items=bidder_out_list,
|
| 94 |
+
page=page,
|
| 95 |
+
page_size=page_size,
|
| 96 |
+
total=total_records
|
| 97 |
+
)
|
| 98 |
+
except Exception as e:
|
| 99 |
+
logger.error(f"Error getting bidders for project {proj_no}: {e}")
|
| 100 |
+
return PaginatedResponse[BidderOut](
|
| 101 |
+
items=[],
|
| 102 |
+
page=page,
|
| 103 |
+
page_size=page_size,
|
| 104 |
+
total=0
|
| 105 |
+
)
|
| 106 |
+
|
| 107 |
+
def get_by_customer_id(self, cust_id: int, page: int = 1, page_size: int = 10,
|
| 108 |
+
order_by: str = "Id", order_dir: str = "ASC") -> PaginatedResponse[BidderOut]:
|
| 109 |
+
"""Get bidders for a specific customer using Bidders table directly."""
|
| 110 |
+
try:
|
| 111 |
+
query = self.db.query(Bidder).filter(Bidder.CustId == cust_id)
|
| 112 |
+
# Validate order_by field
|
| 113 |
+
if not hasattr(Bidder, order_by):
|
| 114 |
+
order_by = "Id"
|
| 115 |
+
order_col = getattr(Bidder, order_by)
|
| 116 |
+
if order_dir.upper() == "DESC":
|
| 117 |
+
order_col = order_col.desc()
|
| 118 |
+
else:
|
| 119 |
+
order_col = order_col.asc()
|
| 120 |
+
query = query.order_by(order_col)
|
| 121 |
+
total_records = query.count()
|
| 122 |
+
bidders = query.offset((page - 1) * page_size).limit(page_size).all()
|
| 123 |
+
bidder_out_list = [self._map_bidder_data(bidder.__dict__) for bidder in bidders]
|
| 124 |
+
return PaginatedResponse[BidderOut](
|
| 125 |
+
items=bidder_out_list,
|
| 126 |
+
page=page,
|
| 127 |
+
page_size=page_size,
|
| 128 |
+
total=total_records
|
| 129 |
+
)
|
| 130 |
+
except Exception as e:
|
| 131 |
+
logger.error(f"Error getting bidders for customer {cust_id}: {e}")
|
| 132 |
+
return PaginatedResponse[BidderOut](
|
| 133 |
+
items=[],
|
| 134 |
+
page=page,
|
| 135 |
+
page_size=page_size,
|
| 136 |
+
total=0
|
| 137 |
+
)
|
| 138 |
+
|
| 139 |
+
def create(self, bidder_data: BidderCreate) -> BidderOut:
|
| 140 |
+
"""Create a new bidder using Bidders table directly."""
|
| 141 |
+
try:
|
| 142 |
+
new_bidder = Bidder(
|
| 143 |
+
ProjNo=bidder_data.proj_no,
|
| 144 |
+
CustId=bidder_data.cust_id,
|
| 145 |
+
Quote=bidder_data.quote,
|
| 146 |
+
Contact=bidder_data.contact,
|
| 147 |
+
Phone=bidder_data.phone,
|
| 148 |
+
Notes=bidder_data.notes,
|
| 149 |
+
DateLastContact=bidder_data.date_last_contact,
|
| 150 |
+
DateFollowup=bidder_data.date_followup,
|
| 151 |
+
Primary=bidder_data.primary or False,
|
| 152 |
+
CustType=bidder_data.cust_type,
|
| 153 |
+
EmailAddress=bidder_data.email_address,
|
| 154 |
+
Fax=bidder_data.fax,
|
| 155 |
+
OrderNr=bidder_data.order_nr,
|
| 156 |
+
CustomerPO=bidder_data.customer_po,
|
| 157 |
+
ShipDate=bidder_data.ship_date,
|
| 158 |
+
DeliverDate=bidder_data.deliver_date,
|
| 159 |
+
ReplacementCost=bidder_data.replacement_cost,
|
| 160 |
+
QuoteDate=bidder_data.quote_date,
|
| 161 |
+
InvoiceDate=bidder_data.invoice_date,
|
| 162 |
+
LessPayment=bidder_data.less_payment,
|
| 163 |
+
Enabled=bidder_data.enabled or True,
|
| 164 |
+
EmployeeId=bidder_data.employee_id
|
| 165 |
+
)
|
| 166 |
+
self.db.add(new_bidder)
|
| 167 |
+
self.db.commit()
|
| 168 |
+
self.db.refresh(new_bidder)
|
| 169 |
+
return self._map_bidder_data(new_bidder.__dict__)
|
| 170 |
+
except SQLAlchemyError as e:
|
| 171 |
+
logger.error(f"Database error creating bidder: {e}")
|
| 172 |
+
self.db.rollback()
|
| 173 |
+
raise
|
| 174 |
+
except Exception as e:
|
| 175 |
+
logger.error(f"Unexpected error creating bidder: {e}")
|
| 176 |
+
self.db.rollback()
|
| 177 |
+
raise
|
| 178 |
+
|
| 179 |
+
def update(self, bidder_id: int, bidder_data: BidderCreate) -> BidderOut:
|
| 180 |
+
"""Update a bidder using Bidders table directly."""
|
| 181 |
+
try:
|
| 182 |
+
bidder = self.db.query(Bidder).filter(Bidder.Id == bidder_id).first()
|
| 183 |
+
if not bidder:
|
| 184 |
+
raise NotFoundException("Bidder not found for update")
|
| 185 |
+
|
| 186 |
+
bidder.ProjNo = bidder_data.proj_no
|
| 187 |
+
bidder.CustId = bidder_data.cust_id
|
| 188 |
+
bidder.Quote = bidder_data.quote
|
| 189 |
+
bidder.Contact = bidder_data.contact
|
| 190 |
+
bidder.Phone = bidder_data.phone
|
| 191 |
+
bidder.Notes = bidder_data.notes
|
| 192 |
+
bidder.DateLastContact = bidder_data.date_last_contact
|
| 193 |
+
bidder.DateFollowup = bidder_data.date_followup
|
| 194 |
+
bidder.Primary = bidder_data.primary or False
|
| 195 |
+
bidder.CustType = bidder_data.cust_type
|
| 196 |
+
bidder.EmailAddress = bidder_data.email_address
|
| 197 |
+
bidder.Fax = bidder_data.fax
|
| 198 |
+
bidder.OrderNr = bidder_data.order_nr
|
| 199 |
+
bidder.CustomerPO = bidder_data.customer_po
|
| 200 |
+
bidder.ShipDate = bidder_data.ship_date
|
| 201 |
+
bidder.DeliverDate = bidder_data.deliver_date
|
| 202 |
+
bidder.ReplacementCost = bidder_data.replacement_cost
|
| 203 |
+
bidder.QuoteDate = bidder_data.quote_date
|
| 204 |
+
bidder.InvoiceDate = bidder_data.invoice_date
|
| 205 |
+
bidder.LessPayment = bidder_data.less_payment
|
| 206 |
+
bidder.Enabled = bidder_data.enabled
|
| 207 |
+
bidder.EmployeeId = bidder_data.employee_id
|
| 208 |
+
|
| 209 |
+
self.db.commit()
|
| 210 |
+
self.db.refresh(bidder)
|
| 211 |
+
return self._map_bidder_data(bidder.__dict__)
|
| 212 |
+
except SQLAlchemyError as e:
|
| 213 |
+
logger.error(f"Database error updating bidder {bidder_id}: {e}")
|
| 214 |
+
self.db.rollback()
|
| 215 |
+
raise
|
| 216 |
+
except Exception as e:
|
| 217 |
+
logger.error(f"Unexpected error updating bidder {bidder_id}: {e}")
|
| 218 |
+
self.db.rollback()
|
| 219 |
+
raise
|
| 220 |
+
|
| 221 |
+
def delete(self, bidder_id: int) -> bool:
|
| 222 |
+
"""Delete a bidder using Bidders table directly."""
|
| 223 |
+
try:
|
| 224 |
+
bidder = self.db.query(Bidder).filter(Bidder.Id == bidder_id).first()
|
| 225 |
+
if not bidder:
|
| 226 |
+
raise NotFoundException("Bidder not found for delete")
|
| 227 |
+
self.db.delete(bidder)
|
| 228 |
+
self.db.commit()
|
| 229 |
+
return True
|
| 230 |
+
except SQLAlchemyError as e:
|
| 231 |
+
logger.error(f"Database error deleting bidder {bidder_id}: {e}")
|
| 232 |
+
self.db.rollback()
|
| 233 |
+
raise
|
| 234 |
+
except Exception as e:
|
| 235 |
+
logger.error(f"Unexpected error deleting bidder {bidder_id}: {e}")
|
| 236 |
+
self.db.rollback()
|
| 237 |
+
raise
|
| 238 |
+
|
| 239 |
+
def _map_bidder_data(self, bidder_data: dict) -> BidderOut:
|
| 240 |
+
"""Map database bidder data to BidderOut schema"""
|
| 241 |
+
# Handle type conversions for proj_no and cust_type
|
| 242 |
+
proj_no_value = bidder_data.get('ProjNo')
|
| 243 |
+
if proj_no_value is not None and not isinstance(proj_no_value, str):
|
| 244 |
+
proj_no_value = str(proj_no_value)
|
| 245 |
+
|
| 246 |
+
cust_type_value = bidder_data.get('CustType')
|
| 247 |
+
if cust_type_value is not None and not isinstance(cust_type_value, str):
|
| 248 |
+
cust_type_value = str(cust_type_value)
|
| 249 |
+
|
| 250 |
+
return BidderOut(
|
| 251 |
+
id=bidder_data.get('Id'),
|
| 252 |
+
proj_no=proj_no_value,
|
| 253 |
+
cust_id=bidder_data.get('CustId'),
|
| 254 |
+
quote=bidder_data.get('Quote'),
|
| 255 |
+
contact=bidder_data.get('Contact'),
|
| 256 |
+
phone=bidder_data.get('Phone'),
|
| 257 |
+
notes=bidder_data.get('Notes'),
|
| 258 |
+
date_last_contact=bidder_data.get('DateLastContact'),
|
| 259 |
+
date_followup=bidder_data.get('DateFollowup'),
|
| 260 |
+
primary=bidder_data.get('Primary'),
|
| 261 |
+
cust_type=cust_type_value,
|
| 262 |
+
email_address=bidder_data.get('EmailAddress'),
|
| 263 |
+
fax=bidder_data.get('Fax'),
|
| 264 |
+
order_nr=bidder_data.get('OrderNr'),
|
| 265 |
+
customer_po=bidder_data.get('CustomerPO'),
|
| 266 |
+
ship_date=bidder_data.get('ShipDate'),
|
| 267 |
+
deliver_date=bidder_data.get('DeliverDate'),
|
| 268 |
+
replacement_cost=bidder_data.get('ReplacementCost'),
|
| 269 |
+
quote_date=bidder_data.get('QuoteDate'),
|
| 270 |
+
invoice_date=bidder_data.get('InvoiceDate'),
|
| 271 |
+
less_payment=bidder_data.get('LessPayment'),
|
| 272 |
+
enabled=bidder_data.get('Enabled'),
|
| 273 |
+
employee_id=bidder_data.get('EmployeeId')
|
| 274 |
+
)
|
app/db/repositories/contact_repo.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
|
|
| 1 |
from sqlalchemy.orm import Session
|
| 2 |
-
from sqlalchemy import text
|
| 3 |
from sqlalchemy.exc import SQLAlchemyError
|
| 4 |
from app.db.models.contact import Contact
|
| 5 |
from app.schemas.contact import ContactCreate, ContactOut
|
|
@@ -15,18 +15,12 @@ class ContactRepository:
|
|
| 15 |
self.db = db
|
| 16 |
|
| 17 |
def get(self, contact_id: int) -> Optional[ContactOut]:
|
| 18 |
-
"""Get a single contact by ID using
|
| 19 |
try:
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
if row:
|
| 26 |
-
columns = result.keys()
|
| 27 |
-
contact_data = dict(zip(columns, row))
|
| 28 |
-
return self._map_contact_data(contact_data)
|
| 29 |
-
return None
|
| 30 |
except SQLAlchemyError as e:
|
| 31 |
logger.error(f"Database error getting contact {contact_id}: {e}")
|
| 32 |
raise
|
|
@@ -34,138 +28,54 @@ class ContactRepository:
|
|
| 34 |
logger.error(f"Unexpected error getting contact {contact_id}: {e}")
|
| 35 |
raise
|
| 36 |
|
| 37 |
-
def list(self, page: int = 1, page_size: int = 10,
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
}
|
| 61 |
-
)
|
| 62 |
-
|
| 63 |
-
# Process the result sets
|
| 64 |
-
if result.returns_rows:
|
| 65 |
-
# First result set contains the contacts
|
| 66 |
-
rows = result.fetchall()
|
| 67 |
-
columns = result.keys()
|
| 68 |
-
|
| 69 |
-
for row in rows:
|
| 70 |
-
contact_data = dict(zip(columns, row))
|
| 71 |
-
contact = self._map_contact_data(contact_data)
|
| 72 |
-
contacts.append(contact)
|
| 73 |
-
|
| 74 |
-
# Move to next result set for total count
|
| 75 |
-
if result.nextset():
|
| 76 |
-
total_row = result.fetchone()
|
| 77 |
-
if total_row:
|
| 78 |
-
total_records = total_row[0] if total_row[0] is not None else len(contacts)
|
| 79 |
-
else:
|
| 80 |
-
total_records = len(contacts)
|
| 81 |
-
else:
|
| 82 |
-
total_records = len(contacts)
|
| 83 |
-
|
| 84 |
-
return PaginatedResponse[ContactOut](
|
| 85 |
-
items=contacts,
|
| 86 |
-
page=page,
|
| 87 |
-
page_size=page_size,
|
| 88 |
-
total=total_records
|
| 89 |
-
)
|
| 90 |
-
|
| 91 |
-
except SQLAlchemyError as e:
|
| 92 |
-
logger.error(f"Database error getting contacts list: {e}")
|
| 93 |
-
# Return empty result instead of raising exception for list operations
|
| 94 |
-
return PaginatedResponse[ContactOut](
|
| 95 |
-
items=[],
|
| 96 |
-
page=page,
|
| 97 |
-
page_size=page_size,
|
| 98 |
-
total=0
|
| 99 |
-
)
|
| 100 |
-
except Exception as e:
|
| 101 |
-
logger.error(f"Unexpected error getting contacts list: {e}")
|
| 102 |
-
# Return empty result instead of raising exception for list operations
|
| 103 |
-
return PaginatedResponse[ContactOut](
|
| 104 |
-
items=[],
|
| 105 |
-
page=page,
|
| 106 |
-
page_size=page_size,
|
| 107 |
-
total=0
|
| 108 |
-
)
|
| 109 |
|
| 110 |
-
def get_by_customer_id(self, customer_id: int, page: int = 1, page_size: int = 10,
|
| 111 |
-
|
| 112 |
-
"""Get contacts for a specific customer using stored procedure"""
|
| 113 |
try:
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
@PageSize = :page_size,
|
| 128 |
-
@TotalRecords = @TotalRecords OUTPUT;
|
| 129 |
-
SELECT @TotalRecords as TotalRecords;"""),
|
| 130 |
-
{
|
| 131 |
-
"customer_id": customer_id,
|
| 132 |
-
"order_by": order_by,
|
| 133 |
-
"order_dir": order_dir,
|
| 134 |
-
"page": page,
|
| 135 |
-
"page_size": page_size
|
| 136 |
-
}
|
| 137 |
-
)
|
| 138 |
-
|
| 139 |
-
if result.returns_rows:
|
| 140 |
-
# First result set contains the contacts
|
| 141 |
-
rows = result.fetchall()
|
| 142 |
-
columns = result.keys()
|
| 143 |
-
|
| 144 |
-
for row in rows:
|
| 145 |
-
contact_data = dict(zip(columns, row))
|
| 146 |
-
contact = self._map_contact_data(contact_data)
|
| 147 |
-
contacts.append(contact)
|
| 148 |
-
|
| 149 |
-
# Move to next result set for total count
|
| 150 |
-
if result.nextset():
|
| 151 |
-
total_row = result.fetchone()
|
| 152 |
-
if total_row:
|
| 153 |
-
total_records = total_row[0] if total_row[0] is not None else len(contacts)
|
| 154 |
-
else:
|
| 155 |
-
total_records = len(contacts)
|
| 156 |
-
else:
|
| 157 |
-
total_records = len(contacts)
|
| 158 |
-
|
| 159 |
return PaginatedResponse[ContactOut](
|
| 160 |
-
items=
|
| 161 |
page=page,
|
| 162 |
page_size=page_size,
|
| 163 |
total=total_records
|
| 164 |
)
|
| 165 |
-
|
| 166 |
except Exception as e:
|
| 167 |
logger.error(f"Error getting contacts for customer {customer_id}: {e}")
|
| 168 |
-
# Return empty result instead of raising exception
|
| 169 |
return PaginatedResponse[ContactOut](
|
| 170 |
items=[],
|
| 171 |
page=page,
|
|
@@ -174,167 +84,97 @@ class ContactRepository:
|
|
| 174 |
)
|
| 175 |
|
| 176 |
def create(self, contact_data: ContactCreate) -> ContactOut:
|
| 177 |
-
"""Create a new contact using
|
| 178 |
try:
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
SELECT @NewContactID as ContactID;"""),
|
| 206 |
-
{
|
| 207 |
-
"customer_id": contact_data.customer_id,
|
| 208 |
-
"first_name": contact_data.first_name,
|
| 209 |
-
"last_name": contact_data.last_name,
|
| 210 |
-
"title": contact_data.title,
|
| 211 |
-
"address": contact_data.address,
|
| 212 |
-
"city": contact_data.city,
|
| 213 |
-
"postal_code": contact_data.postal_code,
|
| 214 |
-
"work_phone": contact_data.work_phone,
|
| 215 |
-
"work_extension": contact_data.work_extension,
|
| 216 |
-
"fax_number": contact_data.fax_number,
|
| 217 |
-
"referred_to": contact_data.referred_to,
|
| 218 |
-
"mobile_phone": contact_data.mobile_phone,
|
| 219 |
-
"christmas_card": contact_data.christmas_card or False,
|
| 220 |
-
"email_address": contact_data.email_address,
|
| 221 |
-
"web_address": contact_data.web_address,
|
| 222 |
-
"customer_type_id": contact_data.customer_type_id,
|
| 223 |
-
"state_id": contact_data.state_id,
|
| 224 |
-
"country_id": contact_data.country_id,
|
| 225 |
-
"temp_address": contact_data.temp_address or False,
|
| 226 |
-
"exported": contact_data.exported or False
|
| 227 |
-
}
|
| 228 |
-
)
|
| 229 |
-
|
| 230 |
-
# Get the new contact ID from the output
|
| 231 |
-
new_contact_id = None
|
| 232 |
-
if result.returns_rows:
|
| 233 |
-
row = result.fetchone()
|
| 234 |
-
if row:
|
| 235 |
-
new_contact_id = row[0]
|
| 236 |
-
|
| 237 |
-
conn.commit()
|
| 238 |
-
|
| 239 |
-
if new_contact_id:
|
| 240 |
-
# Retrieve the newly created contact
|
| 241 |
-
created_contact = self.get(new_contact_id)
|
| 242 |
-
if created_contact:
|
| 243 |
-
return created_contact
|
| 244 |
-
|
| 245 |
-
raise ValueError("Failed to retrieve created contact")
|
| 246 |
-
|
| 247 |
except SQLAlchemyError as e:
|
| 248 |
logger.error(f"Database error creating contact: {e}")
|
|
|
|
| 249 |
raise
|
| 250 |
except Exception as e:
|
| 251 |
logger.error(f"Unexpected error creating contact: {e}")
|
|
|
|
| 252 |
raise
|
| 253 |
|
| 254 |
def update(self, contact_id: int, contact_data: ContactCreate) -> ContactOut:
|
| 255 |
-
"""Update a contact using
|
| 256 |
try:
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
"contact_id": contact_id,
|
| 284 |
-
"customer_id": contact_data.customer_id,
|
| 285 |
-
"first_name": contact_data.first_name,
|
| 286 |
-
"last_name": contact_data.last_name,
|
| 287 |
-
"title": contact_data.title,
|
| 288 |
-
"address": contact_data.address,
|
| 289 |
-
"city": contact_data.city,
|
| 290 |
-
"postal_code": contact_data.postal_code,
|
| 291 |
-
"work_phone": contact_data.work_phone,
|
| 292 |
-
"work_extension": contact_data.work_extension,
|
| 293 |
-
"fax_number": contact_data.fax_number,
|
| 294 |
-
"referred_to": contact_data.referred_to,
|
| 295 |
-
"mobile_phone": contact_data.mobile_phone,
|
| 296 |
-
"christmas_card": contact_data.christmas_card or False,
|
| 297 |
-
"email_address": contact_data.email_address,
|
| 298 |
-
"web_address": contact_data.web_address,
|
| 299 |
-
"customer_type_id": contact_data.customer_type_id,
|
| 300 |
-
"state_id": contact_data.state_id,
|
| 301 |
-
"country_id": contact_data.country_id,
|
| 302 |
-
"temp_address": contact_data.temp_address or False,
|
| 303 |
-
"exported": contact_data.exported or False,
|
| 304 |
-
}
|
| 305 |
-
)
|
| 306 |
-
conn.commit()
|
| 307 |
-
|
| 308 |
-
# Get the updated contact
|
| 309 |
-
updated_contact = self.get(contact_id)
|
| 310 |
-
if not updated_contact:
|
| 311 |
-
raise NotFoundException("Contact not found after update")
|
| 312 |
-
|
| 313 |
-
return updated_contact
|
| 314 |
-
|
| 315 |
except SQLAlchemyError as e:
|
| 316 |
logger.error(f"Database error updating contact {contact_id}: {e}")
|
|
|
|
| 317 |
raise
|
| 318 |
except Exception as e:
|
| 319 |
logger.error(f"Unexpected error updating contact {contact_id}: {e}")
|
|
|
|
| 320 |
raise
|
| 321 |
|
| 322 |
def delete(self, contact_id: int) -> bool:
|
| 323 |
-
"""Delete a contact using
|
| 324 |
try:
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
return True
|
| 332 |
-
|
| 333 |
except SQLAlchemyError as e:
|
| 334 |
logger.error(f"Database error deleting contact {contact_id}: {e}")
|
|
|
|
| 335 |
raise
|
| 336 |
except Exception as e:
|
| 337 |
logger.error(f"Unexpected error deleting contact {contact_id}: {e}")
|
|
|
|
| 338 |
raise
|
| 339 |
|
| 340 |
def _map_contact_data(self, contact_data: dict) -> ContactOut:
|
|
|
|
| 1 |
+
|
| 2 |
from sqlalchemy.orm import Session
|
|
|
|
| 3 |
from sqlalchemy.exc import SQLAlchemyError
|
| 4 |
from app.db.models.contact import Contact
|
| 5 |
from app.schemas.contact import ContactCreate, ContactOut
|
|
|
|
| 15 |
self.db = db
|
| 16 |
|
| 17 |
def get(self, contact_id: int) -> Optional[ContactOut]:
|
| 18 |
+
"""Get a single contact by ID using Contacts table directly."""
|
| 19 |
try:
|
| 20 |
+
contact = self.db.query(Contact).filter(Contact.ContactID == contact_id).first()
|
| 21 |
+
if contact:
|
| 22 |
+
return self._map_contact_data(contact.__dict__)
|
| 23 |
+
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
except SQLAlchemyError as e:
|
| 25 |
logger.error(f"Database error getting contact {contact_id}: {e}")
|
| 26 |
raise
|
|
|
|
| 28 |
logger.error(f"Unexpected error getting contact {contact_id}: {e}")
|
| 29 |
raise
|
| 30 |
|
| 31 |
+
def list(self, page: int = 1, page_size: int = 10, order_by: str = "ContactID", order_direction: str = "ASC", customer_id: int = None) -> PaginatedResponse[ContactOut]:
|
| 32 |
+
"""Get paginated list of all contacts using Contacts table directly, with optional customer_id filter and order by."""
|
| 33 |
+
query = self.db.query(Contact)
|
| 34 |
+
if customer_id is not None:
|
| 35 |
+
query = query.filter(Contact.CustomerID == customer_id)
|
| 36 |
+
# Validate order_by field
|
| 37 |
+
if not hasattr(Contact, order_by):
|
| 38 |
+
order_by = "ContactID"
|
| 39 |
+
order_col = getattr(Contact, order_by)
|
| 40 |
+
if order_direction.upper() == "DESC":
|
| 41 |
+
order_col = order_col.desc()
|
| 42 |
+
else:
|
| 43 |
+
order_col = order_col.asc()
|
| 44 |
+
query = query.order_by(order_col)
|
| 45 |
+
total_records = query.count()
|
| 46 |
+
contacts = query.offset((page - 1) * page_size).limit(page_size).all()
|
| 47 |
+
contact_out_list = [self._map_contact_data(contact.__dict__) for contact in contacts]
|
| 48 |
+
return PaginatedResponse[ContactOut](
|
| 49 |
+
items=contact_out_list,
|
| 50 |
+
page=page,
|
| 51 |
+
page_size=page_size,
|
| 52 |
+
total=total_records
|
| 53 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
|
| 55 |
+
def get_by_customer_id(self, customer_id: int, page: int = 1, page_size: int = 10, order_by: str = "ContactID", order_dir: str = "ASC") -> PaginatedResponse[ContactOut]:
|
| 56 |
+
"""Get contacts for a specific customer using Contacts table directly."""
|
|
|
|
| 57 |
try:
|
| 58 |
+
query = self.db.query(Contact).filter(Contact.CustomerID == customer_id)
|
| 59 |
+
# Validate order_by field
|
| 60 |
+
if not hasattr(Contact, order_by):
|
| 61 |
+
order_by = "ContactID"
|
| 62 |
+
order_col = getattr(Contact, order_by)
|
| 63 |
+
if order_dir.upper() == "DESC":
|
| 64 |
+
order_col = order_col.desc()
|
| 65 |
+
else:
|
| 66 |
+
order_col = order_col.asc()
|
| 67 |
+
query = query.order_by(order_col)
|
| 68 |
+
total_records = query.count()
|
| 69 |
+
contacts = query.offset((page - 1) * page_size).limit(page_size).all()
|
| 70 |
+
contact_out_list = [self._map_contact_data(contact.__dict__) for contact in contacts]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
return PaginatedResponse[ContactOut](
|
| 72 |
+
items=contact_out_list,
|
| 73 |
page=page,
|
| 74 |
page_size=page_size,
|
| 75 |
total=total_records
|
| 76 |
)
|
|
|
|
| 77 |
except Exception as e:
|
| 78 |
logger.error(f"Error getting contacts for customer {customer_id}: {e}")
|
|
|
|
| 79 |
return PaginatedResponse[ContactOut](
|
| 80 |
items=[],
|
| 81 |
page=page,
|
|
|
|
| 84 |
)
|
| 85 |
|
| 86 |
def create(self, contact_data: ContactCreate) -> ContactOut:
|
| 87 |
+
"""Create a new contact using Contacts table directly."""
|
| 88 |
try:
|
| 89 |
+
new_contact = Contact(
|
| 90 |
+
CustomerID=contact_data.customer_id,
|
| 91 |
+
FirstName=contact_data.first_name,
|
| 92 |
+
LastName=contact_data.last_name,
|
| 93 |
+
Title=contact_data.title,
|
| 94 |
+
Address=contact_data.address,
|
| 95 |
+
City=contact_data.city,
|
| 96 |
+
PostalCode=contact_data.postal_code,
|
| 97 |
+
WorkPhone=contact_data.work_phone,
|
| 98 |
+
WorkExtension=contact_data.work_extension,
|
| 99 |
+
FaxNumber=contact_data.fax_number,
|
| 100 |
+
ReferredTo=contact_data.referred_to,
|
| 101 |
+
MobilePhone=contact_data.mobile_phone,
|
| 102 |
+
ChristmasCard=contact_data.christmas_card or False,
|
| 103 |
+
EmailAddress=contact_data.email_address,
|
| 104 |
+
WebAddress=contact_data.web_address,
|
| 105 |
+
CustomerTypeID=contact_data.customer_type_id,
|
| 106 |
+
StateID=contact_data.state_id,
|
| 107 |
+
CountryID=contact_data.country_id,
|
| 108 |
+
TempAddress=contact_data.temp_address or False,
|
| 109 |
+
Exported=contact_data.exported or False
|
| 110 |
+
)
|
| 111 |
+
self.db.add(new_contact)
|
| 112 |
+
self.db.commit()
|
| 113 |
+
self.db.refresh(new_contact)
|
| 114 |
+
return self._map_contact_data(new_contact.__dict__)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
except SQLAlchemyError as e:
|
| 116 |
logger.error(f"Database error creating contact: {e}")
|
| 117 |
+
self.db.rollback()
|
| 118 |
raise
|
| 119 |
except Exception as e:
|
| 120 |
logger.error(f"Unexpected error creating contact: {e}")
|
| 121 |
+
self.db.rollback()
|
| 122 |
raise
|
| 123 |
|
| 124 |
def update(self, contact_id: int, contact_data: ContactCreate) -> ContactOut:
|
| 125 |
+
"""Update a contact using Contacts table directly."""
|
| 126 |
try:
|
| 127 |
+
contact = self.db.query(Contact).filter(Contact.ContactID == contact_id).first()
|
| 128 |
+
if not contact:
|
| 129 |
+
raise NotFoundException("Contact not found for update")
|
| 130 |
+
contact.CustomerID = contact_data.customer_id
|
| 131 |
+
contact.FirstName = contact_data.first_name
|
| 132 |
+
contact.LastName = contact_data.last_name
|
| 133 |
+
contact.Title = contact_data.title
|
| 134 |
+
contact.Address = contact_data.address
|
| 135 |
+
contact.City = contact_data.city
|
| 136 |
+
contact.PostalCode = contact_data.postal_code
|
| 137 |
+
contact.WorkPhone = contact_data.work_phone
|
| 138 |
+
contact.WorkExtension = contact_data.work_extension
|
| 139 |
+
contact.FaxNumber = contact_data.fax_number
|
| 140 |
+
contact.ReferredTo = contact_data.referred_to
|
| 141 |
+
contact.MobilePhone = contact_data.mobile_phone
|
| 142 |
+
contact.ChristmasCard = contact_data.christmas_card or False
|
| 143 |
+
contact.EmailAddress = contact_data.email_address
|
| 144 |
+
contact.WebAddress = contact_data.web_address
|
| 145 |
+
contact.CustomerTypeID = contact_data.customer_type_id
|
| 146 |
+
contact.StateID = contact_data.state_id
|
| 147 |
+
contact.CountryID = contact_data.country_id
|
| 148 |
+
contact.TempAddress = contact_data.temp_address or False
|
| 149 |
+
contact.Exported = contact_data.exported or False
|
| 150 |
+
self.db.commit()
|
| 151 |
+
self.db.refresh(contact)
|
| 152 |
+
return self._map_contact_data(contact.__dict__)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
except SQLAlchemyError as e:
|
| 154 |
logger.error(f"Database error updating contact {contact_id}: {e}")
|
| 155 |
+
self.db.rollback()
|
| 156 |
raise
|
| 157 |
except Exception as e:
|
| 158 |
logger.error(f"Unexpected error updating contact {contact_id}: {e}")
|
| 159 |
+
self.db.rollback()
|
| 160 |
raise
|
| 161 |
|
| 162 |
def delete(self, contact_id: int) -> bool:
|
| 163 |
+
"""Delete a contact using Contacts table directly."""
|
| 164 |
try:
|
| 165 |
+
contact = self.db.query(Contact).filter(Contact.ContactID == contact_id).first()
|
| 166 |
+
if not contact:
|
| 167 |
+
raise NotFoundException("Contact not found for delete")
|
| 168 |
+
self.db.delete(contact)
|
| 169 |
+
self.db.commit()
|
| 170 |
+
return True
|
|
|
|
|
|
|
| 171 |
except SQLAlchemyError as e:
|
| 172 |
logger.error(f"Database error deleting contact {contact_id}: {e}")
|
| 173 |
+
self.db.rollback()
|
| 174 |
raise
|
| 175 |
except Exception as e:
|
| 176 |
logger.error(f"Unexpected error deleting contact {contact_id}: {e}")
|
| 177 |
+
self.db.rollback()
|
| 178 |
raise
|
| 179 |
|
| 180 |
def _map_contact_data(self, contact_data: dict) -> ContactOut:
|
app/prompts/SELECT TOP (1000) [CustomerID].sql
CHANGED
|
@@ -1,19 +1,2 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
,[Address]
|
| 4 |
-
,[City]
|
| 5 |
-
,[PostalCode]
|
| 6 |
-
,[WebAddress]
|
| 7 |
-
,[Referral]
|
| 8 |
-
,[CompanyTypeID]
|
| 9 |
-
,[StateID]
|
| 10 |
-
,[CountryID]
|
| 11 |
-
,[LeadGeneratedFromID]
|
| 12 |
-
,[SpecificSource]
|
| 13 |
-
,[PriorityID]
|
| 14 |
-
,[FollowupDate]
|
| 15 |
-
,[Purchase]
|
| 16 |
-
,[VendorID]
|
| 17 |
-
,[Enabled]
|
| 18 |
-
,[RentalType]
|
| 19 |
-
FROM [hs-prod3].[dbo].[Customers]
|
|
|
|
| 1 |
+
-- Replace 'Bidders' with an existing table name, for example 'Customers'
|
| 2 |
+
select * from dbo.Customers;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/schemas/bidder.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import BaseModel, Field
|
| 2 |
+
from typing import Optional
|
| 3 |
+
from datetime import datetime
|
| 4 |
+
|
| 5 |
+
class BidderCreate(BaseModel):
|
| 6 |
+
proj_no: Optional[str] = Field(None, description="Project number")
|
| 7 |
+
cust_id: Optional[int] = Field(None, description="Customer ID")
|
| 8 |
+
quote: Optional[float] = Field(None, description="Quote amount")
|
| 9 |
+
contact: Optional[str] = Field(None, description="Contact name")
|
| 10 |
+
phone: Optional[str] = Field(None, description="Phone number")
|
| 11 |
+
notes: Optional[str] = Field(None, description="Notes")
|
| 12 |
+
date_last_contact: Optional[datetime] = Field(None, description="Date of last contact")
|
| 13 |
+
date_followup: Optional[datetime] = Field(None, description="Date of follow up")
|
| 14 |
+
primary: Optional[bool] = Field(False, description="Primary bidder flag")
|
| 15 |
+
cust_type: Optional[str] = Field(None, description="Customer type")
|
| 16 |
+
email_address: Optional[str] = Field(None, description="Email address")
|
| 17 |
+
fax: Optional[str] = Field(None, description="Fax number")
|
| 18 |
+
order_nr: Optional[str] = Field(None, description="Order number")
|
| 19 |
+
customer_po: Optional[str] = Field(None, description="Customer purchase order")
|
| 20 |
+
ship_date: Optional[datetime] = Field(None, description="Ship date")
|
| 21 |
+
deliver_date: Optional[datetime] = Field(None, description="Delivery date")
|
| 22 |
+
replacement_cost: Optional[float] = Field(None, description="Replacement cost")
|
| 23 |
+
quote_date: Optional[datetime] = Field(None, description="Quote date")
|
| 24 |
+
invoice_date: Optional[datetime] = Field(None, description="Invoice date")
|
| 25 |
+
less_payment: Optional[float] = Field(None, description="Less payment")
|
| 26 |
+
enabled: Optional[bool] = Field(True, description="Enabled status")
|
| 27 |
+
employee_id: Optional[int] = Field(None, description="Employee ID")
|
| 28 |
+
|
| 29 |
+
class BidderOut(BaseModel):
|
| 30 |
+
id: int = Field(..., description="Bidder ID")
|
| 31 |
+
proj_no: Optional[str] = Field(None, description="Project number")
|
| 32 |
+
cust_id: Optional[int] = Field(None, description="Customer ID")
|
| 33 |
+
quote: Optional[float] = Field(None, description="Quote amount")
|
| 34 |
+
contact: Optional[str] = Field(None, description="Contact name")
|
| 35 |
+
phone: Optional[str] = Field(None, description="Phone number")
|
| 36 |
+
notes: Optional[str] = Field(None, description="Notes")
|
| 37 |
+
date_last_contact: Optional[datetime] = Field(None, description="Date of last contact")
|
| 38 |
+
date_followup: Optional[datetime] = Field(None, description="Date of follow up")
|
| 39 |
+
primary: Optional[bool] = Field(None, description="Primary bidder flag")
|
| 40 |
+
cust_type: Optional[str] = Field(None, description="Customer type")
|
| 41 |
+
email_address: Optional[str] = Field(None, description="Email address")
|
| 42 |
+
fax: Optional[str] = Field(None, description="Fax number")
|
| 43 |
+
order_nr: Optional[str] = Field(None, description="Order number")
|
| 44 |
+
customer_po: Optional[str] = Field(None, description="Customer purchase order")
|
| 45 |
+
ship_date: Optional[datetime] = Field(None, description="Ship date")
|
| 46 |
+
deliver_date: Optional[datetime] = Field(None, description="Delivery date")
|
| 47 |
+
replacement_cost: Optional[float] = Field(None, description="Replacement cost")
|
| 48 |
+
quote_date: Optional[datetime] = Field(None, description="Quote date")
|
| 49 |
+
invoice_date: Optional[datetime] = Field(None, description="Invoice date")
|
| 50 |
+
less_payment: Optional[float] = Field(None, description="Less payment")
|
| 51 |
+
enabled: Optional[bool] = Field(None, description="Enabled status")
|
| 52 |
+
employee_id: Optional[int] = Field(None, description="Employee ID")
|
| 53 |
+
|
| 54 |
+
class Config:
|
| 55 |
+
from_attributes = True # Updated for Pydantic v2
|
app/services/bidder_service.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sqlalchemy.orm import Session
|
| 2 |
+
from app.db.repositories.bidder_repo import BidderRepository
|
| 3 |
+
from app.schemas.bidder import BidderCreate, BidderOut
|
| 4 |
+
from app.schemas.paginated_response import PaginatedResponse
|
| 5 |
+
from app.core.exceptions import NotFoundException
|
| 6 |
+
from typing import List, Optional
|
| 7 |
+
|
| 8 |
+
# Constants
|
| 9 |
+
BIDDER_NOT_FOUND_MSG = "Bidder not found"
|
| 10 |
+
|
| 11 |
+
class BidderService:
|
| 12 |
+
def __init__(self, db: Session):
|
| 13 |
+
self.repo = BidderRepository(db)
|
| 14 |
+
|
| 15 |
+
def get(self, bidder_id: int) -> BidderOut:
|
| 16 |
+
"""Get a single bidder by ID"""
|
| 17 |
+
bidder = self.repo.get(bidder_id)
|
| 18 |
+
if not bidder:
|
| 19 |
+
raise NotFoundException(BIDDER_NOT_FOUND_MSG)
|
| 20 |
+
return bidder
|
| 21 |
+
|
| 22 |
+
def list(self, page: int = 1, page_size: int = 10, order_by: str = "Id",
|
| 23 |
+
order_direction: str = "ASC") -> PaginatedResponse[BidderOut]:
|
| 24 |
+
"""Get a paginated list of all bidders"""
|
| 25 |
+
return self.repo.list(page, page_size, order_by, order_direction)
|
| 26 |
+
|
| 27 |
+
def get_by_project_no(self, proj_no: str, page: int = 1, page_size: int = 10,
|
| 28 |
+
order_by: str = "Id", order_dir: str = "ASC") -> PaginatedResponse[BidderOut]:
|
| 29 |
+
"""Get all bidders for a specific project"""
|
| 30 |
+
return self.repo.get_by_project_no(proj_no, page, page_size, order_by, order_dir)
|
| 31 |
+
|
| 32 |
+
def get_by_customer_id(self, cust_id: int, page: int = 1, page_size: int = 10,
|
| 33 |
+
order_by: str = "Id", order_dir: str = "ASC") -> PaginatedResponse[BidderOut]:
|
| 34 |
+
"""Get all bidders for a specific customer"""
|
| 35 |
+
return self.repo.get_by_customer_id(cust_id, page, page_size, order_by, order_dir)
|
| 36 |
+
|
| 37 |
+
def create(self, data: dict) -> BidderOut:
|
| 38 |
+
"""Create a new bidder"""
|
| 39 |
+
# Validate and clean data
|
| 40 |
+
validated_data = self._validate_bidder_data(data)
|
| 41 |
+
|
| 42 |
+
# Convert dict to BidderCreate for validation
|
| 43 |
+
bidder_data = BidderCreate(**validated_data)
|
| 44 |
+
|
| 45 |
+
return self.repo.create(bidder_data)
|
| 46 |
+
|
| 47 |
+
def update(self, bidder_id: int, data: dict) -> BidderOut:
|
| 48 |
+
"""Update an existing bidder"""
|
| 49 |
+
# Check if bidder exists
|
| 50 |
+
existing_bidder = self.repo.get(bidder_id)
|
| 51 |
+
if not existing_bidder:
|
| 52 |
+
raise NotFoundException(BIDDER_NOT_FOUND_MSG)
|
| 53 |
+
|
| 54 |
+
# Validate and clean data
|
| 55 |
+
validated_data = self._validate_bidder_data(data)
|
| 56 |
+
|
| 57 |
+
# Convert dict to BidderCreate for validation
|
| 58 |
+
bidder_data = BidderCreate(**validated_data)
|
| 59 |
+
|
| 60 |
+
return self.repo.update(bidder_id, bidder_data)
|
| 61 |
+
|
| 62 |
+
def delete(self, bidder_id: int):
|
| 63 |
+
"""Delete a bidder"""
|
| 64 |
+
# Check if bidder exists
|
| 65 |
+
existing_bidder = self.repo.get(bidder_id)
|
| 66 |
+
if not existing_bidder:
|
| 67 |
+
raise NotFoundException(BIDDER_NOT_FOUND_MSG)
|
| 68 |
+
|
| 69 |
+
self.repo.delete(bidder_id)
|
| 70 |
+
|
| 71 |
+
def _validate_bidder_data(self, data: dict) -> dict:
|
| 72 |
+
"""Internal method to validate and clean bidder data"""
|
| 73 |
+
# Ensure project number is provided (required)
|
| 74 |
+
if not data.get('proj_no'):
|
| 75 |
+
raise ValueError("Project number is required")
|
| 76 |
+
|
| 77 |
+
# Clean email format
|
| 78 |
+
if data.get('email_address'):
|
| 79 |
+
email = data['email_address'].strip().lower()
|
| 80 |
+
if '@' not in email:
|
| 81 |
+
raise ValueError("Invalid email address format")
|
| 82 |
+
data['email_address'] = email
|
| 83 |
+
|
| 84 |
+
# Clean phone numbers
|
| 85 |
+
phone_fields = ['phone', 'fax']
|
| 86 |
+
for field in phone_fields:
|
| 87 |
+
if data.get(field):
|
| 88 |
+
# Basic phone number cleaning - keep only allowed characters
|
| 89 |
+
cleaned_phone = ''.join(c for c in data[field] if c.isdigit() or c in '+()-. ')
|
| 90 |
+
data[field] = cleaned_phone.strip()
|
| 91 |
+
|
| 92 |
+
# Ensure quote is a positive number if provided
|
| 93 |
+
if data.get('quote') is not None and data.get('quote') < 0:
|
| 94 |
+
raise ValueError("Quote amount must be a positive number")
|
| 95 |
+
|
| 96 |
+
return data
|