MukeshKapoor25 commited on
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 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 stored procedure"""
19
  try:
20
- with self.db.get_bind().connect() as conn:
21
- result = conn.execute(text("EXEC spContactsGet @ContactID = :contact_id"),
22
- {"contact_id": contact_id})
23
- row = result.fetchone()
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
- order_by: str = "ContactID", order_direction: str = "ASC") -> PaginatedResponse[ContactOut]:
39
- """Get paginated list of all contacts using stored procedure"""
40
- try:
41
- contacts = []
42
- total_records = 0
43
-
44
- with self.db.get_bind().connect() as conn:
45
- # Based on DATABASE_DOCUMENTATION.md, using spContactsGetList
46
- result = conn.execute(
47
- text("""DECLARE @TotalRecords INT;
48
- EXEC spContactsGetList
49
- @OrderBy = :order_by,
50
- @OrderDirection = :order_dir,
51
- @Page = :page,
52
- @PageSize = :page_size,
53
- @TotalRecords = @TotalRecords OUTPUT;
54
- SELECT @TotalRecords as TotalRecords;"""),
55
- {
56
- "order_by": order_by,
57
- "order_dir": order_direction,
58
- "page": page,
59
- "page_size": page_size
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
- order_by: str = "ContactID", order_dir: str = "ASC") -> PaginatedResponse[ContactOut]:
112
- """Get contacts for a specific customer using stored procedure"""
113
  try:
114
- contacts = []
115
- total_records = 0
116
-
117
- with self.db.get_bind().connect() as conn:
118
- # Using spContactsGetListByParam with CustomerID filter
119
- result = conn.execute(
120
- text("""DECLARE @TotalRecords INT;
121
- EXEC spContactsGetListByParam
122
- @CustomerTypeID = NULL,
123
- @CustomerID = :customer_id,
124
- @OrderBy = :order_by,
125
- @OrderDirection = :order_dir,
126
- @Page = :page,
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=contacts,
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 stored procedure"""
178
  try:
179
- with self.db.get_bind().connect() as conn:
180
- # Based on DATABASE_DOCUMENTATION.md, using proper output parameter handling
181
- result = conn.execute(
182
- text("""DECLARE @NewContactID INT;
183
- EXEC spContactsInsert
184
- @CustomerID = :customer_id,
185
- @FirstName = :first_name,
186
- @LastName = :last_name,
187
- @Title = :title,
188
- @Address = :address,
189
- @City = :city,
190
- @PostalCode = :postal_code,
191
- @WorkPhone = :work_phone,
192
- @WorkExtension = :work_extension,
193
- @FaxNumber = :fax_number,
194
- @ReferredTo = :referred_to,
195
- @MobilePhone = :mobile_phone,
196
- @ChristmasCard = :christmas_card,
197
- @EmailAddress = :email_address,
198
- @WebAddress = :web_address,
199
- @CustomerTypeID = :customer_type_id,
200
- @StateID = :state_id,
201
- @CountryID = :country_id,
202
- @TempAddress = :temp_address,
203
- @Exported = :exported,
204
- @ContactID = @NewContactID OUTPUT;
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 stored procedure"""
256
  try:
257
- with self.db.get_bind().connect() as conn:
258
- # Execute the update stored procedure
259
- conn.execute(
260
- text("""EXEC spContactsUpdate
261
- @ContactID = :contact_id,
262
- @CustomerID = :customer_id,
263
- @FirstName = :first_name,
264
- @LastName = :last_name,
265
- @Title = :title,
266
- @Address = :address,
267
- @City = :city,
268
- @PostalCode = :postal_code,
269
- @WorkPhone = :work_phone,
270
- @WorkExtension = :work_extension,
271
- @FaxNumber = :fax_number,
272
- @ReferredTo = :referred_to,
273
- @MobilePhone = :mobile_phone,
274
- @ChristmasCard = :christmas_card,
275
- @EmailAddress = :email_address,
276
- @WebAddress = :web_address,
277
- @CustomerTypeID = :customer_type_id,
278
- @StateID = :state_id,
279
- @CountryID = :country_id,
280
- @TempAddress = :temp_address,
281
- @Exported = :exported"""),
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 stored procedure"""
324
  try:
325
- with self.db.get_bind().connect() as conn:
326
- conn.execute(
327
- text("EXEC spContactsDelete @ContactID = :contact_id"),
328
- {"contact_id": contact_id}
329
- )
330
- conn.commit()
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
- SELECT TOP (1000) [CustomerID]
2
- ,[CompanyName]
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