MukeshKapoor25 commited on
Commit
9b21c17
·
1 Parent(s): 0572149

Add new reference models and update repositories for enhanced data handling

Browse files

- Introduced new models: StatusInfo, EstShipDate, FOB, and EstFreight to expand reference data capabilities.
- Updated existing models (Country, CompanyType, PaymentTerm, etc.) to align with new schema requirements and removed CustomerStatus.
- Modified BidderRepository to streamline project number handling.
- Enhanced ReferenceDataRepository to include new reference models and improved stored procedure handling.
- Updated schemas to reflect changes in models, including new output classes for FOB, EstShipDate, EstFreight, and StatusInfo.
- Refactored ReferenceDataService to incorporate new methods for fetching FOBs, estimated ship dates, estimated freights, and status info.
- Added tools for scanning dynamic SQL in stored procedures, including a SQL script and a Python CLI wrapper.

app/controllers/reference.py CHANGED
@@ -5,7 +5,7 @@ from app.services.reference_service import ReferenceDataService
5
  from app.schemas.reference import (
6
  StateOut, CountryOut, CompanyTypeOut, LeadSourceOut, PaymentTermOut,
7
  PurchasePriceOut, RentalPriceOut, BarrierSizeOut, ProductApplicationOut,
8
- CustomerStatusOut, ProjectStatusOut, ReferenceDataResponse
9
  )
10
  from app.core.dependencies import get_current_user_optional
11
  from app.schemas.auth import CurrentUser
@@ -36,7 +36,6 @@ def get_all_reference_data(
36
  - Rental Prices
37
  - Barrier Sizes
38
  - Product Applications
39
- - Customer Statuses
40
  - Project Statuses
41
 
42
  **Note**: This endpoint is public but may have enhanced data for authenticated users.
@@ -161,31 +160,62 @@ def get_product_applications(
161
  service = ReferenceDataService(db)
162
  return service.get_product_applications(active_only=active_only)
163
 
164
- @router.get("/customer-statuses", response_model=List[CustomerStatusOut])
165
- def get_customer_statuses(
166
- active_only: bool = Query(True, description="Return only active customer statuses"),
 
 
167
  db: Session = Depends(get_db)
168
  ):
169
  """
170
- Get all customer statuses.
171
 
172
- Returns a list of all available customer status options.
173
  """
174
  service = ReferenceDataService(db)
175
- return service.get_customer_statuses(active_only=active_only)
 
176
 
177
- @router.get("/project-statuses", response_model=List[ProjectStatusOut])
178
- def get_project_statuses(
179
- active_only: bool = Query(True, description="Return only active project statuses"),
180
  db: Session = Depends(get_db)
181
  ):
182
  """
183
- Get all project statuses.
184
-
185
- Returns a list of all available project status options with color codes.
 
 
 
 
 
 
 
 
 
 
 
 
186
  """
187
  service = ReferenceDataService(db)
188
- return service.get_project_statuses(active_only=active_only)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
 
190
  @router.post("/clear-cache", status_code=status.HTTP_200_OK)
191
  def clear_reference_cache(
 
5
  from app.schemas.reference import (
6
  StateOut, CountryOut, CompanyTypeOut, LeadSourceOut, PaymentTermOut,
7
  PurchasePriceOut, RentalPriceOut, BarrierSizeOut, ProductApplicationOut,
8
+ ReferenceDataResponse, FOBOut, EstShipDateOut, EstFreightOut, StatusInfoOut
9
  )
10
  from app.core.dependencies import get_current_user_optional
11
  from app.schemas.auth import CurrentUser
 
36
  - Rental Prices
37
  - Barrier Sizes
38
  - Product Applications
 
39
  - Project Statuses
40
 
41
  **Note**: This endpoint is public but may have enhanced data for authenticated users.
 
160
  service = ReferenceDataService(db)
161
  return service.get_product_applications(active_only=active_only)
162
 
163
+
164
+
165
+ @router.get("/fobs", response_model=List[FOBOut])
166
+ def get_FOB(
167
+ active_only: bool = Query(True, description="Return only active FOBS"),
168
  db: Session = Depends(get_db)
169
  ):
170
  """
171
+ Get all FOBs (Free on Board terms).
172
 
173
+ Returns a list of all available FOB terms used in shipping.
174
  """
175
  service = ReferenceDataService(db)
176
+ return service.get_fobs(active_only=active_only)
177
+
178
 
179
+ @router.get("/est-ship-dates", response_model=List[EstShipDateOut])
180
+ def get_est_ship_dates(
181
+ active_only: bool = Query(True, description="Return only active estimated ship dates"),
182
  db: Session = Depends(get_db)
183
  ):
184
  """
185
+ Get all Estimated Ship Date options.
186
+ Returns a list of estimated shipping date options from the DB.
187
+ """
188
+ service = ReferenceDataService(db)
189
+ return service.get_est_ship_dates(active_only=active_only)
190
+
191
+
192
+ @router.get("/est-freights", response_model=List[EstFreightOut])
193
+ def get_est_freights(
194
+ active_only: bool = Query(True, description="Return only active estimated freights"),
195
+ db: Session = Depends(get_db)
196
+ ):
197
+ """
198
+ Get all Estimated Freight options.
199
+ Returns a list of estimated freight options from the DB.
200
  """
201
  service = ReferenceDataService(db)
202
+ return service.get_est_freights(active_only=active_only)
203
+
204
+
205
+ @router.get("/status-info", response_model=List[StatusInfoOut])
206
+ def get_status_info(
207
+ active_only: bool = Query(True, description="Return only active status info records"),
208
+ db: Session = Depends(get_db)
209
+ ):
210
+ """
211
+ Get all StatusInfo records.
212
+ Returns a list of project status metadata (ID, description, abbreviation).
213
+ """
214
+ service = ReferenceDataService(db)
215
+ return service.get_status_info(active_only=active_only)
216
+
217
+
218
+
219
 
220
  @router.post("/clear-cache", status_code=status.HTTP_200_OK)
221
  def clear_reference_cache(
app/db/models/reference.py CHANGED
@@ -1,4 +1,4 @@
1
- from sqlalchemy import Column, Integer, String, Boolean, Text, DateTime
2
  from app.db.base import Base
3
 
4
  class State(Base):
@@ -9,84 +9,93 @@ class State(Base):
9
  description = Column("Description", String(255), nullable=True)
10
  enabled = Column("Enabled", Boolean, nullable=True)
11
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  class Country(Base):
13
  __tablename__ = "Countries"
14
 
15
  country_id = Column("CountryID", Integer, primary_key=True, index=True)
16
- country_name = Column("CountryName", String(100), nullable=False)
17
- country_code = Column("CountryCode", String(3), nullable=True)
18
- # Make IsActive column optional in case it doesn't exist in the database
19
- # Set nullable=True so SQLAlchemy doesn't fail if the column is missing
20
- is_active = Column("IsActive", Boolean, nullable=True)
21
 
22
  class CompanyType(Base):
23
  __tablename__ = "CompanyTypes"
24
 
25
  company_type_id = Column("CompanyTypeID", Integer, primary_key=True, index=True)
26
- company_type_name = Column("CompanyTypeName", String(100), nullable=False)
27
  description = Column("Description", String(255), nullable=True)
28
- is_active = Column("IsActive", Boolean, nullable=False, default=True)
29
 
30
  class LeadGeneratedFrom(Base):
31
  __tablename__ = "LeadGeneratedFroms"
32
 
33
  lead_generated_from_id = Column("LeadGeneratedFromID", Integer, primary_key=True, index=True)
34
- source_name = Column("SourceName", String(100), nullable=False)
35
  description = Column("Description", String(255), nullable=True)
36
- is_active = Column("IsActive", Boolean, nullable=False, default=True)
 
 
37
 
38
  class PaymentTerm(Base):
39
- __tablename__ = "PaymentTerms"
40
-
41
- payment_term_id = Column("PaymentTermID", Integer, primary_key=True, index=True)
42
- term_name = Column("TermName", String(50), nullable=False)
43
- days = Column("Days", Integer, nullable=False)
44
- description = Column("Description", String(255), nullable=True)
45
- is_active = Column("IsActive", Boolean, nullable=False, default=True)
46
 
 
47
  class PurchasePrice(Base):
48
- __tablename__ = "PurchasePrices"
49
 
50
- purchase_price_id = Column("PurchasePriceID", Integer, primary_key=True, index=True)
51
- price_name = Column("PriceName", String(100), nullable=False)
52
- price_value = Column("PriceValue", String(50), nullable=False)
53
- effective_date = Column("EffectiveDate", DateTime, nullable=True)
54
- is_active = Column("IsActive", Boolean, nullable=False, default=True)
55
 
56
  class RentalPrice(Base):
57
- __tablename__ = "RentalPrices"
58
 
59
- rental_price_id = Column("RentalPriceID", Integer, primary_key=True, index=True)
60
- price_name = Column("PriceName", String(100), nullable=False)
61
- price_value = Column("PriceValue", String(50), nullable=False)
62
- effective_date = Column("EffectiveDate", DateTime, nullable=True)
63
- is_active = Column("IsActive", Boolean, nullable=False, default=True)
64
-
65
  class BarrierSize(Base):
66
  __tablename__ = "BarrierSizes"
67
 
68
- barrier_size_id = Column("BarrierSizeID", Integer, primary_key=True, index=True)
69
- size_name = Column("SizeName", String(100), nullable=False)
70
- length = Column("Length", String(20), nullable=True)
71
- height = Column("Height", String(20), nullable=True)
72
- description = Column("Description", String(255), nullable=True)
73
- is_active = Column("IsActive", Boolean, nullable=False, default=True)
 
74
 
75
  class ProductApplication(Base):
76
  __tablename__ = "ProductApplications"
77
 
78
- application_id = Column("ApplicationID", Integer, primary_key=True, index=True)
79
- application_name = Column("ApplicationName", String(100), nullable=False)
80
  description = Column("Description", Text, nullable=True)
81
- is_active = Column("IsActive", Boolean, nullable=False, default=True)
82
 
83
- class CustomerStatus(Base):
84
- __tablename__ = "CustomerStatuses"
85
-
86
- status_id = Column("StatusID", Integer, primary_key=True, index=True)
87
- status_name = Column("StatusName", String(50), nullable=False)
88
- description = Column("Description", String(255), nullable=True)
89
- is_active = Column("IsActive", Boolean, nullable=False, default=True)
90
 
91
  class ProjectStatus(Base):
92
  __tablename__ = "ProjectStatuses"
@@ -95,4 +104,23 @@ class ProjectStatus(Base):
95
  status_name = Column("StatusName", String(50), nullable=False)
96
  description = Column("Description", String(255), nullable=True)
97
  color_code = Column("ColorCode", String(7), nullable=True) # For UI display
98
- is_active = Column("IsActive", Boolean, nullable=False, default=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy import Column, Integer, String, Boolean, Text, DateTime, Numeric
2
  from app.db.base import Base
3
 
4
  class State(Base):
 
9
  description = Column("Description", String(255), nullable=True)
10
  enabled = Column("Enabled", Boolean, nullable=True)
11
 
12
+
13
+ class StatusInfo(Base):
14
+ __tablename__ = "StatusInfo"
15
+
16
+ # DB table uses ID, Desc, Abrv
17
+ status_info_id = Column("ID", Integer, primary_key=True, index=True)
18
+ description = Column("Desc", String(255), nullable=True)
19
+ abrv = Column("Abrv", String(50), nullable=True)
20
+ # optional enabled mapping if present
21
+
22
+
23
+ class EstShipDate(Base):
24
+ __tablename__ = "EstShipDate"
25
+
26
+ # DB table uses Id and EstShipDateDescription
27
+ est_ship_date_id = Column("Id", Integer, primary_key=True, index=True)
28
+ est_ship_date_description = Column("EstShipDateDescription", String(255), nullable=True)
29
+
30
  class Country(Base):
31
  __tablename__ = "Countries"
32
 
33
  country_id = Column("CountryID", Integer, primary_key=True, index=True)
34
+ customer_type_id = Column("CustomerTypeID", Integer, nullable=True)
35
+ description = Column("Description", String(255), nullable=True)
36
+ enabled = Column("Enabled", Boolean, nullable=True)
37
+
38
+
39
 
40
  class CompanyType(Base):
41
  __tablename__ = "CompanyTypes"
42
 
43
  company_type_id = Column("CompanyTypeID", Integer, primary_key=True, index=True)
44
+ customer_type_id = Column("CustomerTypeID", Integer, nullable=True)
45
  description = Column("Description", String(255), nullable=True)
46
+ enabled = Column("Inactive", Boolean, nullable=True)
47
 
48
  class LeadGeneratedFrom(Base):
49
  __tablename__ = "LeadGeneratedFroms"
50
 
51
  lead_generated_from_id = Column("LeadGeneratedFromID", Integer, primary_key=True, index=True)
52
+ source_name = Column("CustomerTypeID", String(100), nullable=False)
53
  description = Column("Description", String(255), nullable=True)
54
+ enabled = Column("Enabled", Boolean, nullable=False, default=True)
55
+
56
+
57
 
58
  class PaymentTerm(Base):
59
+ __tablename__ = "PaymentTerm"
60
+ payment_term_id = Column("ID", Integer, primary_key=True, index=True)
61
+ description = Column("PaymentDescription", String(50), nullable=False)
62
+ enabled = Column("Enabled", Boolean, nullable=False, default=True)
 
 
 
63
 
64
+
65
  class PurchasePrice(Base):
66
+ __tablename__ = "PurchasePrice"
67
 
68
+ purchase_price_id = Column("Id", Integer, primary_key=True, index=True)
69
+ description = Column("Price", String(100), nullable=False)
70
+ enabled = Column("enabled", Boolean, nullable=False, default=True)
 
 
71
 
72
  class RentalPrice(Base):
73
+ __tablename__ = "RentalPrice"
74
 
75
+ rental_price_id = Column("Id", Integer, primary_key=True, index=True)
76
+ description = Column("Price", String(100), nullable=False)
77
+ enabled = Column("Enabled", Boolean, nullable=False, default=True)
78
+
 
 
79
  class BarrierSize(Base):
80
  __tablename__ = "BarrierSizes"
81
 
82
+ barrier_size_id = Column("Id", Integer, primary_key=True, index=True)
83
+ length = Column("Lenght", Numeric(precision=18, scale=2), nullable=True)
84
+ height = Column("Height", Numeric(precision=18, scale=2), nullable=True)
85
+ width = Column("Width", Numeric(precision=18, scale=2), nullable=True)
86
+ cableunits = Column("CableUnits", Numeric(precision=18, scale=2), nullable=True)
87
+ price = Column("Price", Numeric(precision=19, scale=4), nullable=False)
88
+ is_standard = Column("IsStandard", Boolean, nullable=False, default=True)
89
 
90
  class ProductApplication(Base):
91
  __tablename__ = "ProductApplications"
92
 
93
+ application_id = Column("ProductApplicationID", Integer, primary_key=True, index=True)
94
+ customer_type_id = Column("CustomerTypeID", String(100), nullable=False)
95
  description = Column("Description", Text, nullable=True)
96
+ enabled = Column("Enabled", Boolean, nullable=False, default=True)
97
 
98
+
 
 
 
 
 
 
99
 
100
  class ProjectStatus(Base):
101
  __tablename__ = "ProjectStatuses"
 
104
  status_name = Column("StatusName", String(50), nullable=False)
105
  description = Column("Description", String(255), nullable=True)
106
  color_code = Column("ColorCode", String(7), nullable=True) # For UI display
107
+ is_active = Column("IsActive", Boolean, nullable=False, default=True)
108
+
109
+
110
+ class FOB(Base):
111
+ __tablename__ = "FOB"
112
+
113
+ # DB table uses Id and FobDescription (per DB schema)
114
+ fob_id = Column("Id", Integer, primary_key=True, index=True)
115
+ fob_description = Column("FobDescription", String(255), nullable=True)
116
+ # Some reference tables include Enabled/IsActive; include optional mapping
117
+ enabled = Column("Enabled", Boolean, nullable=True)
118
+
119
+
120
+ class EstFreight(Base):
121
+ __tablename__ = "EstFreight"
122
+
123
+ # DB table uses Id and EstFreightDescription
124
+ est_freight_id = Column("Id", Integer, primary_key=True, index=True)
125
+ est_freight_description = Column("EstFreightDescription", String(255), nullable=True)
126
+
app/db/repositories/bidder_repo.py CHANGED
@@ -64,19 +64,15 @@ class BidderRepository:
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"
 
64
  try:
65
  # Handle string or integer proj_no values
66
  # Try both the original value and converted value to handle both cases
67
+
 
68
  # Try to handle numeric project numbers stored in database as int or string
69
  try:
70
+ query = self.db.query(Bidder).filter(
71
+ (Bidder.ProjNo == proj_no)
 
 
 
72
  )
73
  except (ValueError, TypeError):
74
  # If conversion to int fails, just use the original string
75
+ query = self.db.query(Bidder).filter(Bidder.ProjNo == str(proj_no))
76
  # Validate order_by field
77
  if not hasattr(Bidder, order_by):
78
  order_by = "Id"
app/db/repositories/reference_repo.py CHANGED
@@ -2,8 +2,7 @@ from sqlalchemy.orm import Session
2
  from sqlalchemy import text
3
  from app.db.models.reference import (
4
  State, Country, CompanyType, LeadGeneratedFrom, PaymentTerm,
5
- PurchasePrice, RentalPrice, BarrierSize, ProductApplication,
6
- CustomerStatus, ProjectStatus
7
  )
8
  from typing import List, Dict, Any, Optional, Type, Union
9
  from app.core.exceptions import RepositoryException
@@ -32,7 +31,6 @@ class ReferenceDataRepository:
32
  'rental_prices': RentalPrice,
33
  'barrier_sizes': BarrierSize,
34
  'product_applications': ProductApplication,
35
- 'customer_statuses': CustomerStatus,
36
  'project_statuses': ProjectStatus
37
  }
38
 
@@ -43,9 +41,24 @@ class ReferenceDataRepository:
43
  return self._cache[cache_key]
44
 
45
  try:
46
- # Try stored procedure first with required OrderBy parameter
47
- sp_query = text("EXEC spStatesGetList @OrderBy = 'Description'")
48
- result = self.db.execute(sp_query)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
 
50
  if result.returns_rows:
51
  # Map the stored procedure results to our expected schema
@@ -55,9 +68,8 @@ class ReferenceDataRepository:
55
  # Map SP output to our API format
56
  state_data = {
57
  'state_id': row_dict.get('StateID', None),
58
- 'state_name': row_dict.get('Description', None), # Use Description as state_name
59
- 'CustomerTypeID': None, # No state code in DB
60
- 'Enabled': None # No country in DB
61
  }
62
  states_data.append(state_data)
63
 
@@ -68,7 +80,7 @@ class ReferenceDataRepository:
68
  except Exception as e:
69
  logger.warning(f"Stored procedure failed, using fallback: {e}", exc_info=True)
70
 
71
-
72
 
73
  def get_countries(self, active_only: bool = True) -> List[Dict[str, Any]]:
74
  """Get all countries"""
@@ -77,43 +89,45 @@ class ReferenceDataRepository:
77
  return self._cache[cache_key]
78
 
79
  try:
80
- # Try stored procedure first with required OrderBy parameter
81
- sp_query = text("EXEC spCountriesGetList @OrderBy = 'CountryName'")
82
- result = self.db.execute(sp_query)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
 
84
  if result.returns_rows:
85
- countries_data = [dict(row._mapping) for row in result.fetchall()]
86
- # Only filter by IsActive if the column exists in the results
87
- if active_only and any('IsActive' in c for c in countries_data):
88
- countries_data = [c for c in countries_data if c.get('IsActive', True)]
 
 
 
 
 
 
 
89
  self._cache[cache_key] = countries_data
90
  logger.info(f"Retrieved {len(countries_data)} countries via stored procedure")
91
  return countries_data
92
 
93
  except Exception as e:
94
- logger.warning(f"Stored procedure failed, using fallback: {e}")
95
-
96
- # Fallback to direct query
97
- query = self.db.query(Country)
98
- # Only apply active filter if we know the column exists
99
- # Skip active_only filtering since it seems the column might not exist in the DB
100
-
101
- countries = query.all()
102
- countries_data = [
103
- {
104
- 'country_id': country.country_id,
105
- 'country_name': country.country_name,
106
- 'country_code': country.country_code,
107
- # Skip is_active in the response if we can't reliably get it
108
- # 'is_active': getattr(country, 'is_active', True) # Default to True if attribute doesn't exist
109
- }
110
- for country in countries
111
- ]
112
-
113
- self._cache[cache_key] = countries_data
114
- logger.info(f"Retrieved {len(countries_data)} countries via direct query")
115
- return countries_data
116
 
 
 
117
  def get_company_types(self, active_only: bool = True) -> List[Dict[str, Any]]:
118
  """Get all company types"""
119
  cache_key = f"company_types_{active_only}"
@@ -122,15 +136,18 @@ class ReferenceDataRepository:
122
 
123
  query = self.db.query(CompanyType)
124
  if active_only:
125
- query = query.filter(CompanyType.is_active == True)
 
126
 
127
  company_types = query.all()
128
  company_types_data = [
129
  {
130
  'company_type_id': ct.company_type_id,
131
- 'company_type_name': ct.company_type_name,
 
132
  'description': ct.description,
133
- 'is_active': ct.is_active
 
134
  }
135
  for ct in company_types
136
  ]
@@ -147,15 +164,17 @@ class ReferenceDataRepository:
147
 
148
  query = self.db.query(LeadGeneratedFrom)
149
  if active_only:
150
- query = query.filter(LeadGeneratedFrom.is_active == True)
151
 
152
  lead_sources = query.all()
153
  lead_sources_data = [
154
  {
155
  'lead_generated_from_id': ls.lead_generated_from_id,
156
- 'source_name': ls.source_name,
 
 
157
  'description': ls.description,
158
- 'is_active': ls.is_active
159
  }
160
  for ls in lead_sources
161
  ]
@@ -172,16 +191,14 @@ class ReferenceDataRepository:
172
 
173
  query = self.db.query(PaymentTerm)
174
  if active_only:
175
- query = query.filter(PaymentTerm.is_active == True)
176
 
177
  payment_terms = query.all()
178
  payment_terms_data = [
179
  {
180
  'payment_term_id': pt.payment_term_id,
181
- 'term_name': pt.term_name,
182
- 'days': pt.days,
183
  'description': pt.description,
184
- 'is_active': pt.is_active
185
  }
186
  for pt in payment_terms
187
  ]
@@ -198,16 +215,15 @@ class ReferenceDataRepository:
198
 
199
  query = self.db.query(PurchasePrice)
200
  if active_only:
201
- query = query.filter(PurchasePrice.is_active == True)
202
 
203
  purchase_prices = query.all()
204
  purchase_prices_data = [
205
  {
206
  'purchase_price_id': pp.purchase_price_id,
207
- 'price_name': pp.price_name,
208
- 'price_value': pp.price_value,
209
- 'effective_date': pp.effective_date,
210
- 'is_active': pp.is_active
211
  }
212
  for pp in purchase_prices
213
  ]
@@ -224,16 +240,15 @@ class ReferenceDataRepository:
224
 
225
  query = self.db.query(RentalPrice)
226
  if active_only:
227
- query = query.filter(RentalPrice.is_active == True)
228
 
229
  rental_prices = query.all()
230
  rental_prices_data = [
231
  {
232
  'rental_price_id': rp.rental_price_id,
233
- 'price_name': rp.price_name,
234
- 'price_value': rp.price_value,
235
- 'effective_date': rp.effective_date,
236
- 'is_active': rp.is_active
237
  }
238
  for rp in rental_prices
239
  ]
@@ -242,25 +257,27 @@ class ReferenceDataRepository:
242
  logger.info(f"Retrieved {len(rental_prices_data)} rental prices")
243
  return rental_prices_data
244
 
245
- def get_barrier_sizes(self, active_only: bool = True) -> List[Dict[str, Any]]:
246
  """Get all barrier sizes"""
247
- cache_key = f"barrier_sizes_{active_only}"
248
  if cache_key in self._cache:
249
  return self._cache[cache_key]
250
 
251
  query = self.db.query(BarrierSize)
252
- if active_only:
253
- query = query.filter(BarrierSize.is_active == True)
254
-
 
255
  barrier_sizes = query.all()
256
  barrier_sizes_data = [
257
  {
258
  'barrier_size_id': bs.barrier_size_id,
259
- 'size_name': bs.size_name,
260
  'length': bs.length,
261
  'height': bs.height,
262
- 'description': bs.description,
263
- 'is_active': bs.is_active
 
 
264
  }
265
  for bs in barrier_sizes
266
  ]
@@ -277,47 +294,124 @@ class ReferenceDataRepository:
277
 
278
  query = self.db.query(ProductApplication)
279
  if active_only:
280
- query = query.filter(ProductApplication.is_active == True)
281
 
282
  product_applications = query.all()
283
  product_applications_data = [
284
  {
285
  'application_id': pa.application_id,
286
- 'application_name': pa.application_name,
 
287
  'description': pa.description,
288
- 'is_active': pa.is_active
289
  }
290
  for pa in product_applications
291
  ]
292
-
293
  self._cache[cache_key] = product_applications_data
294
  logger.info(f"Retrieved {len(product_applications_data)} product applications")
295
  return product_applications_data
296
 
297
- def get_customer_statuses(self, active_only: bool = True) -> List[Dict[str, Any]]:
298
- """Get all customer statuses"""
299
- cache_key = f"customer_statuses_{active_only}"
 
300
  if cache_key in self._cache:
301
  return self._cache[cache_key]
302
-
303
- query = self.db.query(CustomerStatus)
304
- if active_only:
305
- query = query.filter(CustomerStatus.is_active == True)
306
-
307
- customer_statuses = query.all()
308
- customer_statuses_data = [
309
- {
310
- 'status_id': cs.status_id,
311
- 'status_name': cs.status_name,
312
- 'description': cs.description,
313
- 'is_active': cs.is_active
314
- }
315
- for cs in customer_statuses
316
- ]
317
-
318
- self._cache[cache_key] = customer_statuses_data
319
- logger.info(f"Retrieved {len(customer_statuses_data)} customer statuses")
320
- return customer_statuses_data
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
321
 
322
  def get_project_statuses(self, active_only: bool = True) -> List[Dict[str, Any]]:
323
  """Get all project statuses"""
@@ -357,10 +451,44 @@ class ReferenceDataRepository:
357
  'rental_prices': self.get_rental_prices(active_only),
358
  'barrier_sizes': self.get_barrier_sizes(active_only),
359
  'product_applications': self.get_product_applications(active_only),
360
- 'customer_statuses': self.get_customer_statuses(active_only),
361
- 'project_statuses': self.get_project_statuses(active_only)
 
362
  }
363
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
364
  def clear_cache(self):
365
  """Clear the internal cache to force fresh data on next request"""
366
  self._cache.clear()
 
2
  from sqlalchemy import text
3
  from app.db.models.reference import (
4
  State, Country, CompanyType, LeadGeneratedFrom, PaymentTerm,
5
+ PurchasePrice, RentalPrice, BarrierSize, ProductApplication, ProjectStatus
 
6
  )
7
  from typing import List, Dict, Any, Optional, Type, Union
8
  from app.core.exceptions import RepositoryException
 
31
  'rental_prices': RentalPrice,
32
  'barrier_sizes': BarrierSize,
33
  'product_applications': ProductApplication,
 
34
  'project_statuses': ProjectStatus
35
  }
36
 
 
41
  return self._cache[cache_key]
42
 
43
  try:
44
+ # Try stored procedure first with required OrderBy parameter using SQLAlchemy parameterized text
45
+ sp_query = text("""
46
+ DECLARE @TotalRecords INT;
47
+ EXEC dbo.spStatesGetList
48
+ @OrderBy = :order_by,
49
+ @OrderDirection = :order_dir,
50
+ @Page = :page,
51
+ @PageSize = :page_size,
52
+ @TotalRecords = @TotalRecords OUTPUT;
53
+ SELECT @TotalRecords AS TotalRecords;
54
+ """)
55
+ params = {
56
+ "order_by": "Description",
57
+ "order_dir": "ASC",
58
+ "page": 1,
59
+ "page_size": 1000,
60
+ }
61
+ result = self.db.execute(sp_query, params)
62
 
63
  if result.returns_rows:
64
  # Map the stored procedure results to our expected schema
 
68
  # Map SP output to our API format
69
  state_data = {
70
  'state_id': row_dict.get('StateID', None),
71
+ 'country_name': row_dict.get('Description', None),
72
+ 'is_active': row_dict.get('IsActive', True)
 
73
  }
74
  states_data.append(state_data)
75
 
 
80
  except Exception as e:
81
  logger.warning(f"Stored procedure failed, using fallback: {e}", exc_info=True)
82
 
83
+ return []
84
 
85
  def get_countries(self, active_only: bool = True) -> List[Dict[str, Any]]:
86
  """Get all countries"""
 
89
  return self._cache[cache_key]
90
 
91
  try:
92
+ sp_query = text("""
93
+ DECLARE @TotalRecords INT;
94
+ EXEC dbo.spCountriesGetList
95
+ @OrderBy = :order_by,
96
+ @OrderDirection = :order_dir,
97
+ @Page = :page,
98
+ @PageSize = :page_size,
99
+ @TotalRecords = @TotalRecords OUTPUT;
100
+ SELECT @TotalRecords AS TotalRecords;
101
+ """)
102
+ params = {
103
+ "order_by": "Description",
104
+ "order_dir": "ASC",
105
+ "page": 1,
106
+ "page_size": 1000,
107
+ }
108
+ result = self.db.execute(sp_query, params)
109
 
110
  if result.returns_rows:
111
+ countries_data = []
112
+ for row in result.fetchall():
113
+ row_dict = dict(row._mapping)
114
+ country_data = {
115
+ 'country_id': row_dict.get('CountryID', None),
116
+ 'country_name': row_dict.get('Description', None),
117
+ 'is_active': row_dict.get('IsActive', True)
118
+ }
119
+ countries_data.append(country_data)
120
+ if active_only:
121
+ countries_data = [c for c in countries_data if c.get('is_active', True)]
122
  self._cache[cache_key] = countries_data
123
  logger.info(f"Retrieved {len(countries_data)} countries via stored procedure")
124
  return countries_data
125
 
126
  except Exception as e:
127
+ logger.warning(f"Stored procedure failed, using fallback: {e}", exc_info=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
 
129
+ return []
130
+
131
  def get_company_types(self, active_only: bool = True) -> List[Dict[str, Any]]:
132
  """Get all company types"""
133
  cache_key = f"company_types_{active_only}"
 
136
 
137
  query = self.db.query(CompanyType)
138
  if active_only:
139
+ # The CompanyType model maps the DB "Enabled" column to attribute `enabled`
140
+ query = query.filter(CompanyType.enabled == True)
141
 
142
  company_types = query.all()
143
  company_types_data = [
144
  {
145
  'company_type_id': ct.company_type_id,
146
+ # The DB uses Description for the readable name; expose it as company_type_name
147
+ 'company_type_name': ct.description,
148
  'description': ct.description,
149
+ # Map enabled -> is_active for API consumers
150
+ 'is_active': ct.enabled
151
  }
152
  for ct in company_types
153
  ]
 
164
 
165
  query = self.db.query(LeadGeneratedFrom)
166
  if active_only:
167
+ query = query.filter(LeadGeneratedFrom.enabled == True)
168
 
169
  lead_sources = query.all()
170
  lead_sources_data = [
171
  {
172
  'lead_generated_from_id': ls.lead_generated_from_id,
173
+ # source_name may be stored in a DB column that can contain numeric codes;
174
+ # coerce to string so API consistently returns a string for frontend dropdowns
175
+ 'source_name': str(ls.source_name) if ls.source_name is not None else None,
176
  'description': ls.description,
177
+ 'is_active': ls.enabled
178
  }
179
  for ls in lead_sources
180
  ]
 
191
 
192
  query = self.db.query(PaymentTerm)
193
  if active_only:
194
+ query = query.filter(PaymentTerm.enabled == True)
195
 
196
  payment_terms = query.all()
197
  payment_terms_data = [
198
  {
199
  'payment_term_id': pt.payment_term_id,
 
 
200
  'description': pt.description,
201
+ 'is_active': pt.enabled
202
  }
203
  for pt in payment_terms
204
  ]
 
215
 
216
  query = self.db.query(PurchasePrice)
217
  if active_only:
218
+ query = query.filter(PurchasePrice.enabled == True)
219
 
220
  purchase_prices = query.all()
221
  purchase_prices_data = [
222
  {
223
  'purchase_price_id': pp.purchase_price_id,
224
+ # expose price under 'price' to match PurchasePriceOut schema
225
+ 'price': pp.description,
226
+ 'enabled': pp.enabled
 
227
  }
228
  for pp in purchase_prices
229
  ]
 
240
 
241
  query = self.db.query(RentalPrice)
242
  if active_only:
243
+ query = query.filter(RentalPrice.enabled == True)
244
 
245
  rental_prices = query.all()
246
  rental_prices_data = [
247
  {
248
  'rental_price_id': rp.rental_price_id,
249
+ # expose price under 'price' to match RentalPriceOut schema
250
+ 'price': rp.description,
251
+ 'enabled': rp.enabled
 
252
  }
253
  for rp in rental_prices
254
  ]
 
257
  logger.info(f"Retrieved {len(rental_prices_data)} rental prices")
258
  return rental_prices_data
259
 
260
+ def get_barrier_sizes(self, standard_only: bool = True) -> List[Dict[str, Any]]:
261
  """Get all barrier sizes"""
262
+ cache_key = f"barrier_sizes_{standard_only}"
263
  if cache_key in self._cache:
264
  return self._cache[cache_key]
265
 
266
  query = self.db.query(BarrierSize)
267
+
268
+ if standard_only:
269
+ query = query.filter(BarrierSize.is_standard == True)
270
+
271
  barrier_sizes = query.all()
272
  barrier_sizes_data = [
273
  {
274
  'barrier_size_id': bs.barrier_size_id,
 
275
  'length': bs.length,
276
  'height': bs.height,
277
+ 'width': bs.width,
278
+ 'cableunits': bs.cableunits,
279
+ 'price': bs.price,
280
+ 'is_standard': bs.is_standard
281
  }
282
  for bs in barrier_sizes
283
  ]
 
294
 
295
  query = self.db.query(ProductApplication)
296
  if active_only:
297
+ query = query.filter(ProductApplication.enabled == True)
298
 
299
  product_applications = query.all()
300
  product_applications_data = [
301
  {
302
  'application_id': pa.application_id,
303
+ # customer_type_id may be stored as codes or numbers; expose as string for API
304
+ 'customer_type_id': str(pa.customer_type_id) if pa.customer_type_id is not None else None,
305
  'description': pa.description,
306
+ 'enabled': pa.enabled
307
  }
308
  for pa in product_applications
309
  ]
310
+
311
  self._cache[cache_key] = product_applications_data
312
  logger.info(f"Retrieved {len(product_applications_data)} product applications")
313
  return product_applications_data
314
 
315
+
316
+ def get_fobs(self, active_only: bool = True) -> List[Dict[str, Any]]:
317
+ """Get all FOB (Free On Board) terms"""
318
+ cache_key = f"fobs_{active_only}"
319
  if cache_key in self._cache:
320
  return self._cache[cache_key]
321
+
322
+ # FOB table is small; query directly
323
+ try:
324
+ from app.db.models.reference import FOB
325
+ query = self.db.query(FOB)
326
+ if active_only and hasattr(FOB, 'enabled'):
327
+ query = query.filter(FOB.enabled == True)
328
+
329
+ fobs = query.all()
330
+ fobs_data = [
331
+ {
332
+ 'fob_id': f.fob_id,
333
+ 'fob_description': f.fob_description,
334
+ 'enabled': getattr(f, 'enabled', None)
335
+ }
336
+ for f in fobs
337
+ ]
338
+
339
+ if active_only:
340
+ fobs_data = [f for f in fobs_data if f.get('enabled', True) is not False]
341
+
342
+ self._cache[cache_key] = fobs_data
343
+ logger.info(f"Retrieved {len(fobs_data)} fobs")
344
+ return fobs_data
345
+ except Exception as e:
346
+ logger.warning(f"Failed to query FOB table: {e}", exc_info=True)
347
+ return []
348
+
349
+
350
+ def get_est_ship_dates(self, active_only: bool = True) -> List[Dict[str, Any]]:
351
+ """Get all estimated ship-date options"""
352
+ cache_key = f"est_ship_dates_{active_only}"
353
+ if cache_key in self._cache:
354
+ return self._cache[cache_key]
355
+
356
+ try:
357
+ from app.db.models.reference import EstShipDate
358
+ query = self.db.query(EstShipDate)
359
+ if active_only and hasattr(EstShipDate, 'enabled'):
360
+ query = query.filter(EstShipDate.enabled == True)
361
+
362
+ rows = query.all()
363
+ data = [
364
+ {
365
+ 'est_ship_date_id': r.est_ship_date_id,
366
+ 'est_ship_date_description': r.est_ship_date_description,
367
+ 'enabled': getattr(r, 'enabled', None)
368
+ }
369
+ for r in rows
370
+ ]
371
+
372
+ if active_only:
373
+ data = [d for d in data if d.get('enabled', True) is not False]
374
+
375
+ self._cache[cache_key] = data
376
+ logger.info(f"Retrieved {len(data)} est ship dates")
377
+ return data
378
+ except Exception as e:
379
+ logger.warning(f"Failed to query EstShipDate table: {e}", exc_info=True)
380
+ return []
381
+
382
+
383
+ def get_est_freights(self, active_only: bool = True) -> List[Dict[str, Any]]:
384
+ """Get all estimated freight options"""
385
+ cache_key = f"est_freights_{active_only}"
386
+ if cache_key in self._cache:
387
+ return self._cache[cache_key]
388
+
389
+ try:
390
+ from app.db.models.reference import EstFreight
391
+ query = self.db.query(EstFreight)
392
+ if active_only and hasattr(EstFreight, 'enabled'):
393
+ query = query.filter(EstFreight.enabled == True)
394
+
395
+ rows = query.all()
396
+ data = [
397
+ {
398
+ 'est_freight_id': r.est_freight_id,
399
+ 'est_freight_description': r.est_freight_description,
400
+ 'enabled': getattr(r, 'enabled', None)
401
+ }
402
+ for r in rows
403
+ ]
404
+
405
+ if active_only:
406
+ data = [d for d in data if d.get('enabled', True) is not False]
407
+
408
+ self._cache[cache_key] = data
409
+ logger.info(f"Retrieved {len(data)} est freights")
410
+ return data
411
+ except Exception as e:
412
+ logger.warning(f"Failed to query EstFreight table: {e}", exc_info=True)
413
+ return []
414
+
415
 
416
  def get_project_statuses(self, active_only: bool = True) -> List[Dict[str, Any]]:
417
  """Get all project statuses"""
 
451
  'rental_prices': self.get_rental_prices(active_only),
452
  'barrier_sizes': self.get_barrier_sizes(active_only),
453
  'product_applications': self.get_product_applications(active_only),
454
+ 'fobs': self.get_fobs(active_only),
455
+ 'est_ship_dates': self.get_est_ship_dates(active_only),
456
+ 'est_freights': self.get_est_freights(active_only),
457
  }
458
 
459
+ def get_status_info(self, active_only: bool = True) -> List[Dict[str, Any]]:
460
+ """Get all StatusInfo records"""
461
+ cache_key = f"status_info_{active_only}"
462
+ if cache_key in self._cache:
463
+ return self._cache[cache_key]
464
+
465
+ try:
466
+ from app.db.models.reference import StatusInfo
467
+ query = self.db.query(StatusInfo)
468
+ if active_only and hasattr(StatusInfo, 'enabled'):
469
+ query = query.filter(StatusInfo.enabled == True)
470
+
471
+ rows = query.all()
472
+ data = [
473
+ {
474
+ 'status_info_id': r.status_info_id,
475
+ 'description': r.description,
476
+ 'abrv': r.abrv,
477
+ 'enabled': getattr(r, 'enabled', None)
478
+ }
479
+ for r in rows
480
+ ]
481
+
482
+ if active_only:
483
+ data = [d for d in data if d.get('enabled', True) is not False]
484
+
485
+ self._cache[cache_key] = data
486
+ logger.info(f"Retrieved {len(data)} status info records")
487
+ return data
488
+ except Exception as e:
489
+ logger.warning(f"Failed to query StatusInfo table: {e}", exc_info=True)
490
+ return []
491
+
492
  def clear_cache(self):
493
  """Clear the internal cache to force fresh data on next request"""
494
  self._cache.clear()
app/schemas/reference.py CHANGED
@@ -1,78 +1,102 @@
1
  from pydantic import BaseModel, Field
2
- from typing import Optional, Dict, List, Any
3
  from datetime import datetime
 
 
4
 
5
  class StateOut(BaseModel):
6
  state_id: int = Field(..., description="State ID")
7
- state_name: Optional[str] = Field(None, description="State name (from Description field)")
8
- state_code: Optional[str] = Field(None, description="State code")
9
- country: Optional[str] = Field(None, description="Country")
 
10
 
11
  class CountryOut(BaseModel):
12
  country_id: int = Field(..., description="Country ID")
13
- country_name: str = Field(..., description="Country name")
14
- country_code: Optional[str] = Field(None, description="Country code")
15
- is_active: bool = Field(..., description="Active status")
 
16
 
17
  class CompanyTypeOut(BaseModel):
18
  company_type_id: int = Field(..., description="Company type ID")
19
- company_type_name: str = Field(..., description="Company type name")
20
  description: Optional[str] = Field(None, description="Description")
21
- is_active: bool = Field(..., description="Active status")
 
22
 
23
  class LeadSourceOut(BaseModel):
24
  lead_generated_from_id: int = Field(..., description="Lead source ID")
25
  source_name: str = Field(..., description="Source name")
26
  description: Optional[str] = Field(None, description="Description")
27
- is_active: bool = Field(..., description="Active status")
 
28
 
29
  class PaymentTermOut(BaseModel):
30
  payment_term_id: int = Field(..., description="Payment term ID")
31
- term_name: str = Field(..., description="Term name")
32
- days: int = Field(..., description="Number of days")
33
- description: Optional[str] = Field(None, description="Description")
34
- is_active: bool = Field(..., description="Active status")
35
 
36
  class PurchasePriceOut(BaseModel):
37
  purchase_price_id: int = Field(..., description="Purchase price ID")
38
- price_name: str = Field(..., description="Price name")
39
- price_value: str = Field(..., description="Price value")
40
- effective_date: Optional[datetime] = Field(None, description="Effective date")
41
- is_active: bool = Field(..., description="Active status")
42
 
43
  class RentalPriceOut(BaseModel):
44
  rental_price_id: int = Field(..., description="Rental price ID")
45
- price_name: str = Field(..., description="Price name")
46
- price_value: str = Field(..., description="Price value")
47
- effective_date: Optional[datetime] = Field(None, description="Effective date")
48
- is_active: bool = Field(..., description="Active status")
49
 
50
  class BarrierSizeOut(BaseModel):
51
  barrier_size_id: int = Field(..., description="Barrier size ID")
52
- size_name: str = Field(..., description="Size name")
53
- length: Optional[str] = Field(None, description="Length")
54
- height: Optional[str] = Field(None, description="Height")
55
- description: Optional[str] = Field(None, description="Description")
56
- is_active: bool = Field(..., description="Active status")
 
 
57
 
58
  class ProductApplicationOut(BaseModel):
59
  application_id: int = Field(..., description="Application ID")
60
- application_name: str = Field(..., description="Application name")
61
  description: Optional[str] = Field(None, description="Description")
62
- is_active: bool = Field(..., description="Active status")
63
 
64
- class CustomerStatusOut(BaseModel):
65
- status_id: int = Field(..., description="Status ID")
66
- status_name: str = Field(..., description="Status name")
67
- description: Optional[str] = Field(None, description="Description")
68
- is_active: bool = Field(..., description="Active status")
69
 
70
  class ProjectStatusOut(BaseModel):
71
  status_id: int = Field(..., description="Status ID")
72
  status_name: str = Field(..., description="Status name")
73
  description: Optional[str] = Field(None, description="Description")
74
- color_code: Optional[str] = Field(None, description="Color code for UI")
75
- is_active: bool = Field(..., description="Active status")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
 
77
  class ReferenceDataResponse(BaseModel):
78
  """Complete reference data response containing all lookup tables"""
@@ -85,5 +109,7 @@ class ReferenceDataResponse(BaseModel):
85
  rental_prices: List[RentalPriceOut] = Field(..., description="List of rental prices")
86
  barrier_sizes: List[BarrierSizeOut] = Field(..., description="List of barrier sizes")
87
  product_applications: List[ProductApplicationOut] = Field(..., description="List of product applications")
88
- customer_statuses: List[CustomerStatusOut] = Field(..., description="List of customer statuses")
89
- project_statuses: List[ProjectStatusOut] = Field(..., description="List of project statuses")
 
 
 
1
  from pydantic import BaseModel, Field
2
+ from typing import Optional, List
3
  from datetime import datetime
4
+ from decimal import Decimal
5
+
6
 
7
  class StateOut(BaseModel):
8
  state_id: int = Field(..., description="State ID")
9
+ customer_type_id: Optional[int] = Field(None, description="Customer type ID")
10
+ description: Optional[str] = Field(None, description="Description / state name")
11
+ enabled: Optional[bool] = Field(None, description="Enabled flag")
12
+
13
 
14
  class CountryOut(BaseModel):
15
  country_id: int = Field(..., description="Country ID")
16
+ customer_type_id: Optional[int] = Field(None, description="Customer type ID")
17
+ description: Optional[str] = Field(None, description="Description / country name")
18
+ enabled: Optional[bool] = Field(None, description="Enabled flag")
19
+
20
 
21
  class CompanyTypeOut(BaseModel):
22
  company_type_id: int = Field(..., description="Company type ID")
23
+ customer_type_id: Optional[int] = Field(None, description="Customer type ID")
24
  description: Optional[str] = Field(None, description="Description")
25
+ inactive: Optional[bool] = Field(None, description="Inactive flag (model uses column 'Inactive')")
26
+
27
 
28
  class LeadSourceOut(BaseModel):
29
  lead_generated_from_id: int = Field(..., description="Lead source ID")
30
  source_name: str = Field(..., description="Source name")
31
  description: Optional[str] = Field(None, description="Description")
32
+ enabled: Optional[bool] = Field(True, description="Enabled flag")
33
+
34
 
35
  class PaymentTermOut(BaseModel):
36
  payment_term_id: int = Field(..., description="Payment term ID")
37
+ description: str = Field(..., description="Payment description / term name")
38
+ enabled: Optional[bool] = Field(True, description="Enabled flag")
39
+
 
40
 
41
  class PurchasePriceOut(BaseModel):
42
  purchase_price_id: int = Field(..., description="Purchase price ID")
43
+ price: str = Field(..., description="Price value or description")
44
+ enabled: Optional[bool] = Field(True, description="Enabled flag")
45
+
 
46
 
47
  class RentalPriceOut(BaseModel):
48
  rental_price_id: int = Field(..., description="Rental price ID")
49
+ price: str = Field(..., description="Price value or description")
50
+ enabled: Optional[bool] = Field(True, description="Enabled flag")
51
+
 
52
 
53
  class BarrierSizeOut(BaseModel):
54
  barrier_size_id: int = Field(..., description="Barrier size ID")
55
+ length: Optional[Decimal] = Field(None, description="Length (numeric)")
56
+ height: Optional[Decimal] = Field(None, description="Height (numeric)")
57
+ width: Optional[Decimal] = Field(None, description="Width (numeric)")
58
+ cableunits: Optional[Decimal] = Field(None, description="Cable units (numeric)")
59
+ price: Optional[Decimal] = Field(None, description="Price (numeric)")
60
+ is_standard: Optional[bool] = Field(True, description="Is standard flag")
61
+
62
 
63
  class ProductApplicationOut(BaseModel):
64
  application_id: int = Field(..., description="Application ID")
65
+ customer_type_id: str = Field(..., description="Customer type ID / code")
66
  description: Optional[str] = Field(None, description="Description")
67
+ enabled: Optional[bool] = Field(True, description="Enabled flag")
68
 
 
 
 
 
 
69
 
70
  class ProjectStatusOut(BaseModel):
71
  status_id: int = Field(..., description="Status ID")
72
  status_name: str = Field(..., description="Status name")
73
  description: Optional[str] = Field(None, description="Description")
74
+ color_code: Optional[str] = Field(None, description="Hex color code, e.g. #RRGGBB")
75
+ is_active: Optional[bool] = Field(True, description="Is active flag")
76
+
77
+
78
+ class FOBOut(BaseModel):
79
+ fob_id: int = Field(..., description="FOB ID")
80
+ fob_description: Optional[str] = Field(None, description="FOB description / name")
81
+ enabled: Optional[bool] = Field(True, description="Enabled flag")
82
+
83
+
84
+ class EstShipDateOut(BaseModel):
85
+ est_ship_date_id: int = Field(..., description="Estimated ship date ID")
86
+ est_ship_date_description: Optional[str] = Field(None, description="Estimated ship date description")
87
+ enabled: Optional[bool] = Field(True, description="Enabled flag")
88
+
89
+
90
+ class EstFreightOut(BaseModel):
91
+ est_freight_id: int = Field(..., description="Estimated freight ID")
92
+ est_freight_description: Optional[str] = Field(None, description="Estimated freight description")
93
+ enabled: Optional[bool] = Field(True, description="Enabled flag")
94
+
95
+
96
+ class StatusInfoOut(BaseModel):
97
+ status_info_id: int = Field(..., description="StatusInfo ID")
98
+ description: Optional[str] = Field(None, description="Status description")
99
+ abrv: Optional[str] = Field(None, description="Abbreviation")
100
 
101
  class ReferenceDataResponse(BaseModel):
102
  """Complete reference data response containing all lookup tables"""
 
109
  rental_prices: List[RentalPriceOut] = Field(..., description="List of rental prices")
110
  barrier_sizes: List[BarrierSizeOut] = Field(..., description="List of barrier sizes")
111
  product_applications: List[ProductApplicationOut] = Field(..., description="List of product applications")
112
+ project_statuses: Optional[List[ProjectStatusOut]] = Field([], description="List of project statuses")
113
+ fobs: Optional[List[FOBOut]] = Field([], description="List of FOB (Free On Board) terms")
114
+ est_ship_dates: Optional[List[EstShipDateOut]] = Field([], description="List of Estimated Ship Date options")
115
+ est_freights: Optional[List[EstFreightOut]] = Field([], description="List of Estimated Freight options")
app/services/reference_service.py CHANGED
@@ -3,7 +3,7 @@ from app.db.repositories.reference_repo import ReferenceDataRepository
3
  from app.schemas.reference import (
4
  StateOut, CountryOut, CompanyTypeOut, LeadSourceOut, PaymentTermOut,
5
  PurchasePriceOut, RentalPriceOut, BarrierSizeOut, ProductApplicationOut,
6
- CustomerStatusOut, ProjectStatusOut, ReferenceDataResponse
7
  )
8
  from typing import List, Dict, Any, Optional
9
  import logging
@@ -64,16 +64,32 @@ class ReferenceDataService:
64
  product_applications_data = self.repo.get_product_applications(active_only)
65
  return [ProductApplicationOut(**pa) for pa in product_applications_data]
66
 
67
- def get_customer_statuses(self, active_only: bool = True) -> List[CustomerStatusOut]:
68
- """Get all customer statuses as Pydantic models"""
69
- customer_statuses_data = self.repo.get_customer_statuses(active_only)
70
- return [CustomerStatusOut(**cs) for cs in customer_statuses_data]
71
-
72
- def get_project_statuses(self, active_only: bool = True) -> List[ProjectStatusOut]:
73
- """Get all project statuses as Pydantic models"""
74
- project_statuses_data = self.repo.get_project_statuses(active_only)
75
- return [ProjectStatusOut(**ps) for ps in project_statuses_data]
76
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  def get_all_reference_data(self, active_only: bool = True) -> ReferenceDataResponse:
78
  """
79
  Get all reference data in a single response for efficiency.
@@ -84,20 +100,45 @@ class ReferenceDataService:
84
  # Get all data from repository
85
  all_data = self.repo.get_all_reference_data(active_only)
86
 
87
- # Transform to Pydantic models
88
- return ReferenceDataResponse(
89
- states=[StateOut(**state) for state in all_data['states']],
90
- countries=[CountryOut(**country) for country in all_data['countries']],
91
- company_types=[CompanyTypeOut(**ct) for ct in all_data['company_types']],
92
- lead_sources=[LeadSourceOut(**ls) for ls in all_data['lead_sources']],
93
- payment_terms=[PaymentTermOut(**pt) for pt in all_data['payment_terms']],
94
- purchase_prices=[PurchasePriceOut(**pp) for pp in all_data['purchase_prices']],
95
- rental_prices=[RentalPriceOut(**rp) for rp in all_data['rental_prices']],
96
- barrier_sizes=[BarrierSizeOut(**bs) for bs in all_data['barrier_sizes']],
97
- product_applications=[ProductApplicationOut(**pa) for pa in all_data['product_applications']],
98
- customer_statuses=[CustomerStatusOut(**cs) for cs in all_data['customer_statuses']],
99
- project_statuses=[ProjectStatusOut(**ps) for ps in all_data['project_statuses']]
100
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
  except Exception as e:
102
  logger.error(f"Error retrieving all reference data: {e}")
103
  raise
 
3
  from app.schemas.reference import (
4
  StateOut, CountryOut, CompanyTypeOut, LeadSourceOut, PaymentTermOut,
5
  PurchasePriceOut, RentalPriceOut, BarrierSizeOut, ProductApplicationOut,
6
+ ReferenceDataResponse
7
  )
8
  from typing import List, Dict, Any, Optional
9
  import logging
 
64
  product_applications_data = self.repo.get_product_applications(active_only)
65
  return [ProductApplicationOut(**pa) for pa in product_applications_data]
66
 
67
+ def get_fobs(self, active_only: bool = True):
68
+ """Get all FOBs as Pydantic models"""
69
+ fobs_data = self.repo.get_fobs(active_only)
70
+ # Import here to avoid circular import at module load time
71
+ from app.schemas.reference import FOBOut
72
+ return [FOBOut(**f) for f in fobs_data]
73
+
74
+ def get_est_ship_dates(self, active_only: bool = True):
75
+ """Get all estimated ship-date options as Pydantic models"""
76
+ est_data = self.repo.get_est_ship_dates(active_only)
77
+ from app.schemas.reference import EstShipDateOut
78
+ return [EstShipDateOut(**e) for e in est_data]
79
+
80
+ def get_est_freights(self, active_only: bool = True):
81
+ """Get all estimated freight options as Pydantic models"""
82
+ est_data = self.repo.get_est_freights(active_only)
83
+ from app.schemas.reference import EstFreightOut
84
+ return [EstFreightOut(**e) for e in est_data]
85
+
86
+ def get_status_info(self, active_only: bool = True):
87
+ """Get all StatusInfo records as Pydantic models"""
88
+ data = self.repo.get_status_info(active_only)
89
+ from app.schemas.reference import StatusInfoOut
90
+ return [StatusInfoOut(**d) for d in data]
91
+
92
+
93
  def get_all_reference_data(self, active_only: bool = True) -> ReferenceDataResponse:
94
  """
95
  Get all reference data in a single response for efficiency.
 
100
  # Get all data from repository
101
  all_data = self.repo.get_all_reference_data(active_only)
102
 
103
+ # Transform to Pydantic models using keys provided by the repository.
104
+ # The repository returns a dict with a subset of keys; map only those.
105
+ response_kwargs = {}
106
+ if 'states' in all_data:
107
+ response_kwargs['states'] = [StateOut(**state) for state in all_data['states']]
108
+ if 'countries' in all_data:
109
+ response_kwargs['countries'] = [CountryOut(**country) for country in all_data['countries']]
110
+ if 'company_types' in all_data:
111
+ response_kwargs['company_types'] = [CompanyTypeOut(**ct) for ct in all_data['company_types']]
112
+ if 'lead_sources' in all_data:
113
+ response_kwargs['lead_sources'] = [LeadSourceOut(**ls) for ls in all_data['lead_sources']]
114
+ if 'payment_terms' in all_data:
115
+ response_kwargs['payment_terms'] = [PaymentTermOut(**pt) for pt in all_data['payment_terms']]
116
+ if 'purchase_prices' in all_data:
117
+ response_kwargs['purchase_prices'] = [PurchasePriceOut(**pp) for pp in all_data['purchase_prices']]
118
+ if 'rental_prices' in all_data:
119
+ response_kwargs['rental_prices'] = [RentalPriceOut(**rp) for rp in all_data['rental_prices']]
120
+ if 'barrier_sizes' in all_data:
121
+ response_kwargs['barrier_sizes'] = [BarrierSizeOut(**bs) for bs in all_data['barrier_sizes']]
122
+ if 'product_applications' in all_data:
123
+ response_kwargs['product_applications'] = [ProductApplicationOut(**pa) for pa in all_data['product_applications']]
124
+ if 'fobs' in all_data:
125
+ from app.schemas.reference import FOBOut
126
+ response_kwargs['fobs'] = [FOBOut(**fb) for fb in all_data['fobs']]
127
+ if 'est_ship_dates' in all_data:
128
+ from app.schemas.reference import EstShipDateOut
129
+ response_kwargs['est_ship_dates'] = [EstShipDateOut(**ed) for ed in all_data['est_ship_dates']]
130
+ if 'est_freights' in all_data:
131
+ from app.schemas.reference import EstFreightOut
132
+ response_kwargs['est_freights'] = [EstFreightOut(**ef) for ef in all_data['est_freights']]
133
+ if 'status_info' in all_data:
134
+ from app.schemas.reference import StatusInfoOut
135
+ response_kwargs['status_info'] = [StatusInfoOut(**si) for si in all_data['status_info']]
136
+ # project_statuses and other optional tables may be added by the repo later
137
+ if 'project_statuses' in all_data:
138
+ from app.schemas.reference import ProjectStatusOut
139
+ response_kwargs['project_statuses'] = [ProjectStatusOut(**ps) for ps in all_data['project_statuses']]
140
+
141
+ return ReferenceDataResponse(**response_kwargs)
142
  except Exception as e:
143
  logger.error(f"Error retrieving all reference data: {e}")
144
  raise
tools/README_FIND_DYNAMIC_SQL.md ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Find dynamic SQL in stored procedures
2
+
3
+ Files
4
+ - find_dynamic_sql_procs.sql: parameterized T-SQL scanner. Use in SSMS, Azure Data Studio, or sqlcmd.
5
+ - find_dynamic_sql_procs.py: small CLI wrapper that runs the SQL and emits CSV.
6
+
7
+ Quick examples
8
+
9
+ 1) Run the SQL directly (Azure Data Studio/SSMS):
10
+
11
+ -- edit variables at top of file or use client substitution
12
+ :setvar TopN 200
13
+ :setvar ContextChars 120
14
+
15
+ -- open and run the file
16
+
17
+ 2) Run via python wrapper (recommended for automation):
18
+
19
+ ```bash
20
+ python tools/find_dynamic_sql_procs.py --server demo.azonix.in --database hs-prod3 --uid myuser --pwd "mypassword" --top 200 > dynamic_procs.csv
21
+ ```
22
+
23
+ Notes & limitations
24
+ - The scanner uses simple LIKE patterns to detect likely dynamic SQL usage (sp_executesql, EXEC(@var), string concatenation).
25
+ - It prioritizes precision via a small confidence score but will miss obfuscated dynamic SQL and may return false positives from comments or similar tokens.
26
+ - Combine the output with manual inspection of `definition` for highest accuracy.
27
+ - The Python wrapper requires `pyodbc` and an appropriate ODBC driver (e.g. ODBC Driver 17/18 for SQL Server).
28
+
29
+ Security
30
+ - Avoid putting credentials in shell history. Prefer using a secure credential store or trusted connections.
31
+
32
+ Next steps (optional)
33
+ - Add a mode to export full proc definitions to files.
34
+ - Add a deeper parser using tSQLt or ANTLR for T-SQL to more accurately detect dynamic SQL boundaries.
tools/find_dynamic_sql_procs.py ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """tools/find_dynamic_sql_procs.py
2
+ Simple CLI wrapper that runs the T-SQL scanner against a SQL Server database using pyodbc.
3
+ Usage:
4
+ python tools/find_dynamic_sql_procs.py --server demo.azonix.in --database hs-prod3 --uid user --pwd pass
5
+
6
+ It prints a CSV to stdout with basic columns and confidence score.
7
+
8
+ Notes:
9
+ - Requires pyodbc installed in your environment.
10
+ - Use Windows authentication by omitting uid/pwd and passing --trusted.
11
+ """
12
+ import argparse
13
+ import csv
14
+ import sys
15
+ import pyodbc
16
+
17
+ import os
18
+
19
+ SQL_PATH = os.path.join(os.path.dirname(__file__), 'find_dynamic_sql_procs.sql')
20
+ SQL_TEMPLATE = open(SQL_PATH, 'r', encoding='utf-8').read()
21
+
22
+ def run_scan(conn_str, top=1000):
23
+ sql = SQL_TEMPLATE.replace('TOP(@Top)', f'TOP({top})') if 'TOP(@Top)' in SQL_TEMPLATE else SQL_TEMPLATE
24
+ with pyodbc.connect(conn_str, autocommit=True) as cn:
25
+ cur = cn.cursor()
26
+ cur.execute(sql)
27
+ cols = [c[0] for c in cur.description]
28
+ writer = csv.writer(sys.stdout)
29
+ writer.writerow(cols)
30
+ for row in cur:
31
+ writer.writerow(row)
32
+
33
+
34
+ if __name__ == '__main__':
35
+ ap = argparse.ArgumentParser(description='Find procs using dynamic SQL patterns')
36
+ ap.add_argument('--server', required=True)
37
+ ap.add_argument('--database', required=True)
38
+ ap.add_argument('--uid')
39
+ ap.add_argument('--pwd')
40
+ ap.add_argument('--trusted', action='store_true')
41
+ ap.add_argument('--top', type=int, default=1000)
42
+ args = ap.parse_args()
43
+
44
+ if args.trusted:
45
+ conn = f'DRIVER={{ODBC Driver 17 for SQL Server}};SERVER={args.server};DATABASE={args.database};Trusted_Connection=yes;'
46
+ else:
47
+ if not args.uid or not args.pwd:
48
+ ap.error('Either --trusted or both --uid and --pwd are required')
49
+ conn = f'DRIVER={{ODBC Driver 17 for SQL Server}};SERVER={args.server};DATABASE={args.database};UID={args.uid};PWD={args.pwd};'
50
+
51
+ run_scan(conn, top=args.top)
tools/find_dynamic_sql_procs.sql ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- tools/find_dynamic_sql_procs.sql
2
+ -- Parameterized T-SQL to scan all stored procedure sources for dynamic-SQL patterns.
3
+ -- Usage:
4
+ -- :setvar SearchPattern "sp_executesql|EXEC\s*\(|EXEC\s+@|\+\s*'"
5
+ -- :setvar TopN 1000
6
+ -- :setvar ContextChars 120
7
+ -- Run in SSMS, Azure Data Studio or sqlcmd (sqlcmd: use -v to pass variables).
8
+
9
+ SET NOCOUNT ON;
10
+
11
+ -- variables via sqlcmd or client substitution
12
+ DECLARE @SearchPattern NVARCHAR(400) = 'sp_executesql|EXEC\s*\(|EXEC\s+@|\+\s*\''; -- regex-like tokens (plain LIKE uses %)
13
+ DECLARE @Top INT = 1000;
14
+ DECLARE @Context INT = 120; -- characters of surrounding context
15
+
16
+ -- We use simple LIKE checks plus more specific patterns to score confidence.
17
+
18
+ SELECT TOP(@Top)
19
+ s.name AS schema_name,
20
+ p.name AS proc_name,
21
+ p.create_date,
22
+ p.modify_date,
23
+ m.definition,
24
+ -- basic token flags
25
+ CASE WHEN m.definition LIKE '%sp_executesql%' THEN 1 ELSE 0 END AS has_sp_executesql,
26
+ CASE WHEN m.definition LIKE '%EXEC %(%' ESCAPE '\' THEN 1 ELSE 0 END AS has_exec_parenthesis,
27
+ CASE WHEN m.definition LIKE '%EXEC @%' THEN 1 ELSE 0 END AS has_exec_variable,
28
+ CASE WHEN m.definition LIKE '%''+%''%' ESCAPE '\' OR m.definition LIKE '%'' + %' ESCAPE '\' THEN 1 ELSE 0 END AS has_string_concat,
29
+ -- rough confidence score
30
+ (CASE WHEN m.definition LIKE '%sp_executesql%' THEN 3 ELSE 0 END
31
+ + CASE WHEN m.definition LIKE '%EXEC %(%' ESCAPE '\' THEN 2 ELSE 0 END
32
+ + CASE WHEN m.definition LIKE '%EXEC @%' THEN 2 ELSE 0 END
33
+ + CASE WHEN m.definition LIKE '%''+%''%' ESCAPE '\' OR m.definition LIKE '%'' + %' ESCAPE '\' THEN 1 ELSE 0 END) AS confidence_score
34
+ FROM sys.procedures p
35
+ JOIN sys.schemas s ON p.schema_id = s.schema_id
36
+ LEFT JOIN sys.sql_modules m ON p.object_id = m.object_id
37
+ WHERE m.definition IS NOT NULL
38
+ AND (
39
+ m.definition LIKE '%sp_executesql%'
40
+ OR m.definition LIKE '%EXEC %(%' ESCAPE '\'
41
+ OR m.definition LIKE '%EXEC @%'
42
+ OR m.definition LIKE '%''+%''%' ESCAPE '\'
43
+ OR m.definition LIKE '%'' + %' ESCAPE '\'
44
+ )
45
+ ORDER BY confidence_score DESC, s.name, p.name;