Spaces:
Paused
feat(reference): add customer_type_id filtering to reference data endpoints
Browse files- Add customer_type_id query parameter to get_all_reference_data endpoint
- Add customer_type_id query parameter to get_company_types endpoint
- Add customer_type_id query parameter to get_lead_sources endpoint
- Refactor company_types repository method to use direct SQL queries with parameter binding
- Refactor lead_sources repository method to use direct SQL queries with parameter binding
- Update active_only filter logic to include NULL values for enabled/inactive flags
- Improve cross-version database compatibility by using text() queries instead of ORM
- Add proper field name-based mapping for query result handling
- Update cache key generation to include customer_type_id parameter
- Enhance logging messages for better debugging of query failures
|
@@ -26,6 +26,7 @@ router = APIRouter(prefix="/api/v1/reference", tags=["reference-data"])
|
|
| 26 |
@router.get("/all", response_model=ReferenceDataResponse)
|
| 27 |
def get_all_reference_data(
|
| 28 |
active_only: bool = Query(True, description="Return only active records"),
|
|
|
|
| 29 |
db: Session = Depends(get_db),
|
| 30 |
current_user: Optional[CurrentUser] = Depends(get_current_user_optional)
|
| 31 |
):
|
|
@@ -48,7 +49,7 @@ def get_all_reference_data(
|
|
| 48 |
**Note**: This endpoint is public but may have enhanced data for authenticated users.
|
| 49 |
"""
|
| 50 |
service = ReferenceDataService(db)
|
| 51 |
-
return service.get_all_reference_data(active_only=active_only)
|
| 52 |
|
| 53 |
@router.get("/states", response_model=List[StateOut])
|
| 54 |
def get_states(
|
|
|
|
| 26 |
@router.get("/all", response_model=ReferenceDataResponse)
|
| 27 |
def get_all_reference_data(
|
| 28 |
active_only: bool = Query(True, description="Return only active records"),
|
| 29 |
+
customer_type_id: Optional[int] = Query(None, description="Filter by customer type ID"),
|
| 30 |
db: Session = Depends(get_db),
|
| 31 |
current_user: Optional[CurrentUser] = Depends(get_current_user_optional)
|
| 32 |
):
|
|
|
|
| 49 |
**Note**: This endpoint is public but may have enhanced data for authenticated users.
|
| 50 |
"""
|
| 51 |
service = ReferenceDataService(db)
|
| 52 |
+
return service.get_all_reference_data(active_only=active_only, customer_type_id=customer_type_id)
|
| 53 |
|
| 54 |
@router.get("/states", response_model=List[StateOut])
|
| 55 |
def get_states(
|
|
@@ -43,7 +43,7 @@ class ReferenceDataRepository:
|
|
| 43 |
return self._cache[cache_key]
|
| 44 |
|
| 45 |
try:
|
| 46 |
-
# Use direct SQL query
|
| 47 |
query_str = """
|
| 48 |
SELECT TOP (1000) [StateID], [CustomerTypeID], [Description], [Enabled]
|
| 49 |
FROM [hs-prod3].[dbo].[States]
|
|
@@ -52,7 +52,7 @@ class ReferenceDataRepository:
|
|
| 52 |
params = {}
|
| 53 |
|
| 54 |
if active_only:
|
| 55 |
-
query_str += " AND [Enabled] = 1"
|
| 56 |
|
| 57 |
if customer_type_id is not None:
|
| 58 |
query_str += " AND [CustomerTypeID] = :customer_type_id"
|
|
@@ -61,10 +61,10 @@ class ReferenceDataRepository:
|
|
| 61 |
sql_query = text(query_str)
|
| 62 |
result = self.db.execute(sql_query, params)
|
| 63 |
|
|
|
|
| 64 |
if result.returns_rows:
|
| 65 |
-
# Map the query results to our expected schema
|
| 66 |
-
states_data = []
|
| 67 |
for row in result.fetchall():
|
|
|
|
| 68 |
row_dict = dict(row._mapping)
|
| 69 |
# Map query output to our API format
|
| 70 |
state_data = {
|
|
@@ -80,7 +80,7 @@ class ReferenceDataRepository:
|
|
| 80 |
return states_data
|
| 81 |
|
| 82 |
except Exception as e:
|
| 83 |
-
logger.warning(f"Direct query failed: {e}", exc_info=True)
|
| 84 |
|
| 85 |
return []
|
| 86 |
|
|
@@ -118,30 +118,46 @@ class ReferenceDataRepository:
|
|
| 118 |
if cache_key in self._cache:
|
| 119 |
return self._cache[cache_key]
|
| 120 |
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
|
| 146 |
def get_lead_sources(self, active_only: bool = True, customer_type_id: Optional[int] = None) -> List[Dict[str, Any]]:
|
| 147 |
"""Get all lead generation sources, optionally filtered by customer_type_id"""
|
|
@@ -149,27 +165,46 @@ class ReferenceDataRepository:
|
|
| 149 |
if cache_key in self._cache:
|
| 150 |
return self._cache[cache_key]
|
| 151 |
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 173 |
|
| 174 |
def get_payment_terms(self, active_only: bool = True) -> List[Dict[str, Any]]:
|
| 175 |
"""Get all payment terms"""
|
|
@@ -393,13 +428,16 @@ class ReferenceDataRepository:
|
|
| 393 |
"""Get all project statuses - Temporarily disabled due to model issues"""
|
| 394 |
return [] # Return empty list for now
|
| 395 |
|
| 396 |
-
def get_all_reference_data(self, active_only: bool = True) -> Dict[str, List[Dict[str, Any]]]:
|
| 397 |
"""Get all reference data in a single call for efficiency"""
|
|
|
|
|
|
|
|
|
|
| 398 |
return {
|
| 399 |
-
'states': self.get_states(active_only),
|
| 400 |
'countries': self.get_countries(active_only),
|
| 401 |
-
'company_types': self.get_company_types(active_only),
|
| 402 |
-
'lead_sources': self.get_lead_sources(active_only),
|
| 403 |
'payment_terms': self.get_payment_terms(active_only),
|
| 404 |
'purchase_prices': self.get_purchase_prices(active_only),
|
| 405 |
'rental_prices': self.get_rental_prices(active_only),
|
|
@@ -409,7 +447,7 @@ class ReferenceDataRepository:
|
|
| 409 |
'est_ship_dates': self.get_est_ship_dates(active_only),
|
| 410 |
'est_freights': self.get_est_freights(active_only),
|
| 411 |
'status_info': self.get_status_info(active_only),
|
| 412 |
-
'priorities': self.get_priorities(
|
| 413 |
'phone_types': self.get_phone_types(active_only),
|
| 414 |
}
|
| 415 |
|
|
@@ -447,31 +485,46 @@ class ReferenceDataRepository:
|
|
| 447 |
return []
|
| 448 |
|
| 449 |
def get_priorities(self, active_only: bool = True, customer_type_id: Optional[int] = None) -> List[Dict[str, Any]]:
|
| 450 |
-
"""Get all priorities, filtered by
|
| 451 |
cache_key = f"priorities_{active_only}_{customer_type_id}"
|
| 452 |
if cache_key in self._cache:
|
| 453 |
return self._cache[cache_key]
|
| 454 |
|
| 455 |
-
|
| 456 |
-
|
|
|
|
| 457 |
SELECT [PriorityID], [CustomerTypeID], [Description]
|
| 458 |
-
FROM [Priorities]
|
| 459 |
-
WHERE
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 475 |
|
| 476 |
def get_phone_types(self, active_only: bool = False) -> List[Dict[str, Any]]:
|
| 477 |
"""Get all phone types"""
|
|
|
|
| 43 |
return self._cache[cache_key]
|
| 44 |
|
| 45 |
try:
|
| 46 |
+
# Use direct SQL query with proper parameter binding for cross-version compatibility
|
| 47 |
query_str = """
|
| 48 |
SELECT TOP (1000) [StateID], [CustomerTypeID], [Description], [Enabled]
|
| 49 |
FROM [hs-prod3].[dbo].[States]
|
|
|
|
| 52 |
params = {}
|
| 53 |
|
| 54 |
if active_only:
|
| 55 |
+
query_str += " AND ([Enabled] = 1 OR [Enabled] IS NULL)"
|
| 56 |
|
| 57 |
if customer_type_id is not None:
|
| 58 |
query_str += " AND [CustomerTypeID] = :customer_type_id"
|
|
|
|
| 61 |
sql_query = text(query_str)
|
| 62 |
result = self.db.execute(sql_query, params)
|
| 63 |
|
| 64 |
+
states_data = []
|
| 65 |
if result.returns_rows:
|
|
|
|
|
|
|
| 66 |
for row in result.fetchall():
|
| 67 |
+
# Use field name-based mapping for proper structure
|
| 68 |
row_dict = dict(row._mapping)
|
| 69 |
# Map query output to our API format
|
| 70 |
state_data = {
|
|
|
|
| 80 |
return states_data
|
| 81 |
|
| 82 |
except Exception as e:
|
| 83 |
+
logger.warning(f"Direct states query failed: {e}", exc_info=True)
|
| 84 |
|
| 85 |
return []
|
| 86 |
|
|
|
|
| 118 |
if cache_key in self._cache:
|
| 119 |
return self._cache[cache_key]
|
| 120 |
|
| 121 |
+
try:
|
| 122 |
+
# Use direct SQL query for better compatibility across Python versions
|
| 123 |
+
query_str = """
|
| 124 |
+
SELECT [CompanyTypeID], [CustomerTypeID], [Description], [Inactive]
|
| 125 |
+
FROM [CompanyTypes]
|
| 126 |
+
WHERE 1=1
|
| 127 |
+
"""
|
| 128 |
+
params = {}
|
| 129 |
+
|
| 130 |
+
if active_only:
|
| 131 |
+
# When active_only is True, return only active records (Inactive == 0 or NULL)
|
| 132 |
+
query_str += " AND ([Inactive] = 0 OR [Inactive] IS NULL)"
|
| 133 |
+
|
| 134 |
+
if customer_type_id is not None:
|
| 135 |
+
query_str += " AND [CustomerTypeID] = :customer_type_id"
|
| 136 |
+
params["customer_type_id"] = customer_type_id
|
| 137 |
+
|
| 138 |
+
sql_query = text(query_str)
|
| 139 |
+
result = self.db.execute(sql_query, params)
|
| 140 |
+
|
| 141 |
+
company_types_data = []
|
| 142 |
+
if result.returns_rows:
|
| 143 |
+
for row in result.fetchall():
|
| 144 |
+
# Use field name-based mapping for proper structure
|
| 145 |
+
row_dict = dict(row._mapping)
|
| 146 |
+
company_type_data = {
|
| 147 |
+
'company_type_id': row_dict.get('CompanyTypeID', None),
|
| 148 |
+
'customer_type_id': row_dict.get('CustomerTypeID', None),
|
| 149 |
+
'description': row_dict.get('Description', None),
|
| 150 |
+
'inactive': row_dict.get('Inactive', False),
|
| 151 |
+
}
|
| 152 |
+
company_types_data.append(company_type_data)
|
| 153 |
+
|
| 154 |
+
self._cache[cache_key] = company_types_data
|
| 155 |
+
logger.info(f"Retrieved {len(company_types_data)} company types via direct query")
|
| 156 |
+
return company_types_data
|
| 157 |
+
|
| 158 |
+
except Exception as e:
|
| 159 |
+
logger.warning(f"Direct company types query failed: {e}", exc_info=True)
|
| 160 |
+
return []
|
| 161 |
|
| 162 |
def get_lead_sources(self, active_only: bool = True, customer_type_id: Optional[int] = None) -> List[Dict[str, Any]]:
|
| 163 |
"""Get all lead generation sources, optionally filtered by customer_type_id"""
|
|
|
|
| 165 |
if cache_key in self._cache:
|
| 166 |
return self._cache[cache_key]
|
| 167 |
|
| 168 |
+
try:
|
| 169 |
+
# Use direct SQL query for better compatibility across Python versions
|
| 170 |
+
query_str = """
|
| 171 |
+
SELECT [LeadGeneratedFromID], [CustomerTypeID], [Description], [Enabled]
|
| 172 |
+
FROM [LeadGeneratedFroms]
|
| 173 |
+
WHERE 1=1
|
| 174 |
+
"""
|
| 175 |
+
params = {}
|
| 176 |
+
|
| 177 |
+
if active_only:
|
| 178 |
+
query_str += " AND ([Enabled] = 1 OR [Enabled] IS NULL)"
|
| 179 |
+
|
| 180 |
+
if customer_type_id is not None:
|
| 181 |
+
query_str += " AND [CustomerTypeID] = :customer_type_id"
|
| 182 |
+
params["customer_type_id"] = customer_type_id
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
sql_query = text(query_str)
|
| 186 |
+
result = self.db.execute(sql_query, params)
|
| 187 |
+
|
| 188 |
+
lead_sources_data = []
|
| 189 |
+
if result.returns_rows:
|
| 190 |
+
for row in result.fetchall():
|
| 191 |
+
# Use field name-based mapping for proper structure
|
| 192 |
+
row_dict = dict(row._mapping)
|
| 193 |
+
lead_source_data = {
|
| 194 |
+
'lead_generated_from_id': row_dict.get('LeadGeneratedFromID', None),
|
| 195 |
+
'customer_type_id': row_dict.get('CustomerTypeID', None),
|
| 196 |
+
'description': row_dict.get('Description', None),
|
| 197 |
+
'enabled': row_dict.get('Enabled', True)
|
| 198 |
+
}
|
| 199 |
+
lead_sources_data.append(lead_source_data)
|
| 200 |
+
|
| 201 |
+
self._cache[cache_key] = lead_sources_data
|
| 202 |
+
logger.info(f"Retrieved {len(lead_sources_data)} lead sources via direct query")
|
| 203 |
+
return lead_sources_data
|
| 204 |
+
|
| 205 |
+
except Exception as e:
|
| 206 |
+
logger.warning(f"Direct lead sources query failed: {e}", exc_info=True)
|
| 207 |
+
return []
|
| 208 |
|
| 209 |
def get_payment_terms(self, active_only: bool = True) -> List[Dict[str, Any]]:
|
| 210 |
"""Get all payment terms"""
|
|
|
|
| 428 |
"""Get all project statuses - Temporarily disabled due to model issues"""
|
| 429 |
return [] # Return empty list for now
|
| 430 |
|
| 431 |
+
def get_all_reference_data(self, active_only: bool = True, customer_type_id: Optional[int] = None) -> Dict[str, List[Dict[str, Any]]]:
|
| 432 |
"""Get all reference data in a single call for efficiency"""
|
| 433 |
+
# Use customer_type_id = 1 as default if not provided (for backward compatibility)
|
| 434 |
+
default_customer_type_id = customer_type_id if customer_type_id is not None else 1
|
| 435 |
+
|
| 436 |
return {
|
| 437 |
+
'states': self.get_states(active_only, default_customer_type_id),
|
| 438 |
'countries': self.get_countries(active_only),
|
| 439 |
+
'company_types': self.get_company_types(active_only, default_customer_type_id),
|
| 440 |
+
'lead_sources': self.get_lead_sources(active_only, default_customer_type_id),
|
| 441 |
'payment_terms': self.get_payment_terms(active_only),
|
| 442 |
'purchase_prices': self.get_purchase_prices(active_only),
|
| 443 |
'rental_prices': self.get_rental_prices(active_only),
|
|
|
|
| 447 |
'est_ship_dates': self.get_est_ship_dates(active_only),
|
| 448 |
'est_freights': self.get_est_freights(active_only),
|
| 449 |
'status_info': self.get_status_info(active_only),
|
| 450 |
+
'priorities': self.get_priorities(active_only, default_customer_type_id),
|
| 451 |
'phone_types': self.get_phone_types(active_only),
|
| 452 |
}
|
| 453 |
|
|
|
|
| 485 |
return []
|
| 486 |
|
| 487 |
def get_priorities(self, active_only: bool = True, customer_type_id: Optional[int] = None) -> List[Dict[str, Any]]:
|
| 488 |
+
"""Get all priorities, optionally filtered by customer_type_id"""
|
| 489 |
cache_key = f"priorities_{active_only}_{customer_type_id}"
|
| 490 |
if cache_key in self._cache:
|
| 491 |
return self._cache[cache_key]
|
| 492 |
|
| 493 |
+
try:
|
| 494 |
+
# Use direct SQL query with proper parameter binding
|
| 495 |
+
query_str = """
|
| 496 |
SELECT [PriorityID], [CustomerTypeID], [Description]
|
| 497 |
+
FROM [Priorities]
|
| 498 |
+
WHERE 1=1
|
| 499 |
+
"""
|
| 500 |
+
params = {}
|
| 501 |
+
|
| 502 |
+
if customer_type_id is not None:
|
| 503 |
+
query_str += " AND [CustomerTypeID] = :customer_type_id"
|
| 504 |
+
params["customer_type_id"] = customer_type_id
|
| 505 |
+
|
| 506 |
+
sql_query = text(query_str)
|
| 507 |
+
result = self.db.execute(sql_query, params)
|
| 508 |
+
|
| 509 |
+
priorities_data = []
|
| 510 |
+
if result.returns_rows:
|
| 511 |
+
for row in result.fetchall():
|
| 512 |
+
# Use field name-based mapping for proper structure
|
| 513 |
+
row_dict = dict(row._mapping)
|
| 514 |
+
priority_data = {
|
| 515 |
+
'priority_id': row_dict.get('PriorityID', None),
|
| 516 |
+
'customer_type_id': row_dict.get('CustomerTypeID', None),
|
| 517 |
+
'description': row_dict.get('Description', None)
|
| 518 |
+
}
|
| 519 |
+
priorities_data.append(priority_data)
|
| 520 |
+
|
| 521 |
+
self._cache[cache_key] = priorities_data
|
| 522 |
+
logger.info(f"Retrieved {len(priorities_data)} priorities via direct query")
|
| 523 |
+
return priorities_data
|
| 524 |
+
|
| 525 |
+
except Exception as e:
|
| 526 |
+
logger.warning(f"Direct priorities query failed: {e}", exc_info=True)
|
| 527 |
+
return []
|
| 528 |
|
| 529 |
def get_phone_types(self, active_only: bool = False) -> List[Dict[str, Any]]:
|
| 530 |
"""Get all phone types"""
|
|
@@ -425,7 +425,7 @@ class ReferenceDataService:
|
|
| 425 |
)
|
| 426 |
|
| 427 |
|
| 428 |
-
def get_all_reference_data(self, active_only: bool = True) -> ReferenceDataResponse:
|
| 429 |
"""
|
| 430 |
Get all reference data in a single response for efficiency.
|
| 431 |
This is useful for frontend applications that need to populate
|
|
@@ -433,7 +433,7 @@ class ReferenceDataService:
|
|
| 433 |
"""
|
| 434 |
try:
|
| 435 |
# Get all data from repository
|
| 436 |
-
all_data = self.repo.get_all_reference_data(active_only)
|
| 437 |
|
| 438 |
# Transform to Pydantic models using keys provided by the repository.
|
| 439 |
# The repository returns a dict with a subset of keys; map only those.
|
|
|
|
| 425 |
)
|
| 426 |
|
| 427 |
|
| 428 |
+
def get_all_reference_data(self, active_only: bool = True, customer_type_id: Optional[int] = None) -> ReferenceDataResponse:
|
| 429 |
"""
|
| 430 |
Get all reference data in a single response for efficiency.
|
| 431 |
This is useful for frontend applications that need to populate
|
|
|
|
| 433 |
"""
|
| 434 |
try:
|
| 435 |
# Get all data from repository
|
| 436 |
+
all_data = self.repo.get_all_reference_data(active_only, customer_type_id)
|
| 437 |
|
| 438 |
# Transform to Pydantic models using keys provided by the repository.
|
| 439 |
# The repository returns a dict with a subset of keys; map only those.
|