MukeshKapoor25 commited on
Commit
8c83744
·
1 Parent(s): 5bcd6a3

feat(contact): add ContactAddress model and repository for managing contact addresses

Browse files

feat(contact): update ContactRepository to handle address_id linking
feat(contact): enhance ContactCreate and ContactOut schemas with address_id field
refactor(report): update ForeignKey reference for project_id in Report model
refactor(barrier_sizes): remove extend_existing table argument for clarity
refactor(bidders_barrier_sizes): remove extend_existing table argument for clarity

app/db/models/barrier_size.py CHANGED
@@ -3,7 +3,6 @@ from app.db.base import Base
3
 
4
  class BarrierSizes(Base):
5
  __tablename__ = "BarrierSizes"
6
- __table_args__ = {'extend_existing': True}
7
 
8
  Id = Column(Integer, primary_key=True, index=True, autoincrement=True)
9
  Height = Column(Float, nullable=True)
 
3
 
4
  class BarrierSizes(Base):
5
  __tablename__ = "BarrierSizes"
 
6
 
7
  Id = Column(Integer, primary_key=True, index=True, autoincrement=True)
8
  Height = Column(Float, nullable=True)
app/db/models/bidders_barrier_sizes.py CHANGED
@@ -4,7 +4,6 @@ from app.db.base import Base
4
 
5
  class BiddersBarrierSizes(Base):
6
  __tablename__ = "BiddersBarrierSizes"
7
- __table_args__ = {'extend_existing': True}
8
 
9
  Id = Column(Integer, primary_key=True, index=True, autoincrement=True)
10
  InventoryId = Column(Integer, nullable=True) # Added InventoryId field
 
4
 
5
  class BiddersBarrierSizes(Base):
6
  __tablename__ = "BiddersBarrierSizes"
 
7
 
8
  Id = Column(Integer, primary_key=True, index=True, autoincrement=True)
9
  InventoryId = Column(Integer, nullable=True) # Added InventoryId field
app/db/models/contact_address.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy import Column, Integer, Boolean
2
+ from app.db.base import Base
3
+
4
+
5
+ class ContactAddress(Base):
6
+ __tablename__ = "ContactAddress"
7
+
8
+ Id = Column(Integer, primary_key=True, index=True)
9
+ AddressId = Column(Integer, nullable=False)
10
+ ContactId = Column(Integer, nullable=False)
11
+ Enabled = Column(Boolean, nullable=False, default=True)
app/db/models/report.py CHANGED
@@ -5,7 +5,7 @@ from datetime import datetime
5
  class Report(Base):
6
  __tablename__ = "reports"
7
  id = Column(Integer, primary_key=True, index=True)
8
- project_id = Column(Integer, ForeignKey("projects.id"), nullable=False)
9
  type = Column(String(50), nullable=False)
10
  status = Column(String(50), nullable=False, default="pending")
11
  file_path = Column(String(255), nullable=True)
 
5
  class Report(Base):
6
  __tablename__ = "reports"
7
  id = Column(Integer, primary_key=True, index=True)
8
+ project_id = Column(Integer, ForeignKey("Projects.ProjectNo"), nullable=False)
9
  type = Column(String(50), nullable=False)
10
  status = Column(String(50), nullable=False, default="pending")
11
  file_path = Column(String(255), nullable=True)
app/db/repositories/contact_address_repo.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy.orm import Session
2
+ from sqlalchemy.exc import SQLAlchemyError
3
+ from typing import Optional
4
+ import logging
5
+
6
+ from app.db.models.contact_address import ContactAddress
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ class ContactAddressRepository:
12
+ def __init__(self, db: Session):
13
+ self.db = db
14
+
15
+ def upsert_for_contact(self, contact_id: int, address_id: int, enabled: Optional[bool] = True) -> ContactAddress:
16
+ """Create or update ContactAddress for a given contact.
17
+
18
+ - If a record exists for `ContactId`, update `AddressId` and `Enabled`.
19
+ - Otherwise, insert a new row.
20
+ """
21
+ try:
22
+ ca: Optional[ContactAddress] = (
23
+ self.db.query(ContactAddress)
24
+ .filter(ContactAddress.ContactId == contact_id)
25
+ .first()
26
+ )
27
+ if ca:
28
+ ca.AddressId = address_id
29
+ if enabled is not None:
30
+ ca.Enabled = enabled
31
+ else:
32
+ ca = ContactAddress(AddressId=address_id, ContactId=contact_id, Enabled=bool(enabled))
33
+ self.db.add(ca)
34
+
35
+ self.db.commit()
36
+ self.db.refresh(ca)
37
+ return ca
38
+ except SQLAlchemyError as e:
39
+ logger.error(f"Database error upserting ContactAddress for contact {contact_id}: {e}")
40
+ self.db.rollback()
41
+ raise
42
+ except Exception as e:
43
+ logger.error(f"Unexpected error upserting ContactAddress for contact {contact_id}: {e}")
44
+ self.db.rollback()
45
+ raise
app/db/repositories/contact_repo.py CHANGED
@@ -3,6 +3,7 @@ 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
 
6
  from app.schemas.paginated_response import PaginatedResponse
7
  from app.core.exceptions import NotFoundException
8
  from typing import List, Optional
@@ -19,7 +20,7 @@ class ContactRepository:
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}")
@@ -44,7 +45,7 @@ class ContactRepository:
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,
@@ -67,7 +68,7 @@ class ContactRepository:
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,
@@ -111,7 +112,14 @@ class ContactRepository:
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()
@@ -149,7 +157,14 @@ class ContactRepository:
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()
@@ -177,30 +192,37 @@ class ContactRepository:
177
  self.db.rollback()
178
  raise
179
 
180
- def _map_contact_data(self, contact_data: dict) -> ContactOut:
181
  """Map database contact data to ContactOut schema"""
 
 
 
 
 
 
182
  return ContactOut(
183
- contact_id=contact_data.get('ContactID'),
184
- customer_id=contact_data.get('CustomerID'),
185
- first_name=contact_data.get('FirstName'),
186
- last_name=contact_data.get('LastName'),
187
- title=contact_data.get('Title'),
188
- address=contact_data.get('Address'),
189
- city=contact_data.get('City'),
190
- postal_code=contact_data.get('PostalCode'),
191
- work_phone=contact_data.get('WorkPhone'),
192
- work_extension=contact_data.get('WorkExtension'),
193
- fax_number=contact_data.get('FaxNumber'),
194
- referred_to=contact_data.get('ReferredTo'),
195
- mobile_phone=contact_data.get('MobilePhone'),
196
- christmas_card=contact_data.get('ChristmasCard'),
197
- email_address=contact_data.get('EmailAddress'),
198
- web_address=contact_data.get('WebAddress'),
199
- customer_type_id=contact_data.get('CustomerTypeID'),
200
- state_id=contact_data.get('StateID'),
201
- country_id=contact_data.get('CountryID'),
202
- temp_address=contact_data.get('TempAddress'),
203
- exported=contact_data.get('Exported'),
204
- enabled=contact_data.get('Enabled'),
205
- send_literature=contact_data.get('SendLiterature')
 
206
  )
 
3
  from sqlalchemy.exc import SQLAlchemyError
4
  from app.db.models.contact import Contact
5
  from app.schemas.contact import ContactCreate, ContactOut
6
+ from app.db.repositories.contact_address_repo import ContactAddressRepository
7
  from app.schemas.paginated_response import PaginatedResponse
8
  from app.core.exceptions import NotFoundException
9
  from typing import List, Optional
 
20
  try:
21
  contact = self.db.query(Contact).filter(Contact.ContactID == contact_id).first()
22
  if contact:
23
+ return self._map_contact_data(contact)
24
  return None
25
  except SQLAlchemyError as e:
26
  logger.error(f"Database error getting contact {contact_id}: {e}")
 
45
  query = query.order_by(order_col)
46
  total_records = query.count()
47
  contacts = query.offset((page - 1) * page_size).limit(page_size).all()
48
+ contact_out_list = [self._map_contact_data(contact) for contact in contacts]
49
  return PaginatedResponse[ContactOut](
50
  items=contact_out_list,
51
  page=page,
 
68
  query = query.order_by(order_col)
69
  total_records = query.count()
70
  contacts = query.offset((page - 1) * page_size).limit(page_size).all()
71
+ contact_out_list = [self._map_contact_data(contact) for contact in contacts]
72
  return PaginatedResponse[ContactOut](
73
  items=contact_out_list,
74
  page=page,
 
112
  self.db.add(new_contact)
113
  self.db.commit()
114
  self.db.refresh(new_contact)
115
+ # If address_id provided by UI, upsert ContactAddress link
116
+ if contact_data.address_id is not None:
117
+ ContactAddressRepository(self.db).upsert_for_contact(
118
+ contact_id=new_contact.ContactID,
119
+ address_id=contact_data.address_id,
120
+ enabled=True,
121
+ )
122
+ return self._map_contact_data(new_contact)
123
  except SQLAlchemyError as e:
124
  logger.error(f"Database error creating contact: {e}")
125
  self.db.rollback()
 
157
  contact.Exported = contact_data.exported or False
158
  self.db.commit()
159
  self.db.refresh(contact)
160
+ # If address_id provided by UI, upsert ContactAddress link
161
+ if contact_data.address_id is not None:
162
+ ContactAddressRepository(self.db).upsert_for_contact(
163
+ contact_id=contact.ContactID,
164
+ address_id=contact_data.address_id,
165
+ enabled=True,
166
+ )
167
+ return self._map_contact_data(contact)
168
  except SQLAlchemyError as e:
169
  logger.error(f"Database error updating contact {contact_id}: {e}")
170
  self.db.rollback()
 
192
  self.db.rollback()
193
  raise
194
 
195
+ def _map_contact_data(self, contact_data) -> ContactOut:
196
  """Map database contact data to ContactOut schema"""
197
+ # Handle both dict and SQLAlchemy object
198
+ if isinstance(contact_data, dict):
199
+ contact_obj = type('ContactObj', (), contact_data)()
200
+ else:
201
+ contact_obj = contact_data
202
+
203
  return ContactOut(
204
+ contact_id=getattr(contact_obj, 'ContactID', None),
205
+ customer_id=getattr(contact_obj, 'CustomerID', None),
206
+ first_name=getattr(contact_obj, 'FirstName', None),
207
+ last_name=getattr(contact_obj, 'LastName', None),
208
+ title=getattr(contact_obj, 'Title', None),
209
+ address=getattr(contact_obj, 'Address', None),
210
+ address_id=None, # Not stored in Contacts table
211
+ city=getattr(contact_obj, 'City', None),
212
+ postal_code=getattr(contact_obj, 'PostalCode', None),
213
+ work_phone=getattr(contact_obj, 'WorkPhone', None),
214
+ work_extension=getattr(contact_obj, 'WorkExtension', None),
215
+ fax_number=getattr(contact_obj, 'FaxNumber', None),
216
+ referred_to=getattr(contact_obj, 'ReferredTo', None),
217
+ mobile_phone=getattr(contact_obj, 'MobilePhone', None),
218
+ christmas_card=getattr(contact_obj, 'ChristmasCard', None),
219
+ email_address=getattr(contact_obj, 'EmailAddress', None),
220
+ web_address=getattr(contact_obj, 'WebAddress', None),
221
+ customer_type_id=getattr(contact_obj, 'CustomerTypeID', None),
222
+ state_id=getattr(contact_obj, 'StateID', None),
223
+ country_id=getattr(contact_obj, 'CountryID', None),
224
+ temp_address=getattr(contact_obj, 'TempAddress', None),
225
+ exported=getattr(contact_obj, 'Exported', None),
226
+ enabled=getattr(contact_obj, 'Enabled', None),
227
+ send_literature=getattr(contact_obj, 'SendLiterature', None)
228
  )
app/schemas/contact.py CHANGED
@@ -9,6 +9,7 @@ class ContactCreate(BaseModel):
9
  last_name: Optional[str] = Field(None, max_length=200, description="Contact last name")
10
  title: Optional[str] = Field(None, max_length=40, description="Contact title/position")
11
  address: Optional[str] = Field(None, max_length=200, description="Contact address")
 
12
  city: Optional[str] = Field(None, max_length=30, description="Contact city")
13
  postal_code: Optional[str] = Field(None, max_length=70, description="Contact postal code")
14
  work_phone: Optional[str] = Field(None, max_length=50, description="Work phone number")
@@ -34,6 +35,7 @@ class ContactOut(BaseModel):
34
  last_name: Optional[str] = Field(None, description="Contact last name")
35
  title: Optional[str] = Field(None, description="Contact title/position")
36
  address: Optional[str] = Field(None, description="Contact address")
 
37
  city: Optional[str] = Field(None, description="Contact city")
38
  postal_code: Optional[str] = Field(None, description="Contact postal code")
39
  work_phone: Optional[str] = Field(None, description="Work phone number")
 
9
  last_name: Optional[str] = Field(None, max_length=200, description="Contact last name")
10
  title: Optional[str] = Field(None, max_length=40, description="Contact title/position")
11
  address: Optional[str] = Field(None, max_length=200, description="Contact address")
12
+ address_id: Optional[int] = Field(None, description="Linked address ID (ContactAddress)")
13
  city: Optional[str] = Field(None, max_length=30, description="Contact city")
14
  postal_code: Optional[str] = Field(None, max_length=70, description="Contact postal code")
15
  work_phone: Optional[str] = Field(None, max_length=50, description="Work phone number")
 
35
  last_name: Optional[str] = Field(None, description="Contact last name")
36
  title: Optional[str] = Field(None, description="Contact title/position")
37
  address: Optional[str] = Field(None, description="Contact address")
38
+ address_id: Optional[int] = Field(None, description="Linked address ID (ContactAddress)")
39
  city: Optional[str] = Field(None, description="Contact city")
40
  postal_code: Optional[str] = Field(None, description="Contact postal code")
41
  work_phone: Optional[str] = Field(None, description="Work phone number")
tests/unit/conftest.py CHANGED
@@ -6,6 +6,20 @@ from app.db.base import Base
6
 
7
  # Ensure models are imported so metadata is populated for create_all
8
  import app.db.models.address # noqa: F401
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
 
11
  @pytest.fixture
 
6
 
7
  # Ensure models are imported so metadata is populated for create_all
8
  import app.db.models.address # noqa: F401
9
+ import app.db.models.barrier_size # noqa: F401
10
+ import app.db.models.bidder # noqa: F401
11
+ import app.db.models.bidder_contact # noqa: F401
12
+ import app.db.models.bidders_barrier_sizes # noqa: F401
13
+ import app.db.models.contact # noqa: F401
14
+ import app.db.models.contact_address # noqa: F401
15
+ import app.db.models.customer # noqa: F401
16
+ import app.db.models.distributor # noqa: F401
17
+ import app.db.models.employee # noqa: F401
18
+ import app.db.models.project # noqa: F401
19
+ import app.db.models.project_related # noqa: F401
20
+ import app.db.models.reference # noqa: F401
21
+ import app.db.models.report # noqa: F401
22
+ import app.db.models.user # noqa: F401
23
 
24
 
25
  @pytest.fixture