Spaces:
Paused
Paused
Commit ·
8c83744
1
Parent(s): 5bcd6a3
feat(contact): add ContactAddress model and repository for managing contact addresses
Browse filesfeat(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 +0 -1
- app/db/models/bidders_barrier_sizes.py +0 -1
- app/db/models/contact_address.py +11 -0
- app/db/models/report.py +1 -1
- app/db/repositories/contact_address_repo.py +45 -0
- app/db/repositories/contact_repo.py +51 -29
- app/schemas/contact.py +2 -0
- tests/unit/conftest.py +14 -0
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("
|
| 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
|
| 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
|
| 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
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 181 |
"""Map database contact data to ContactOut schema"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
return ContactOut(
|
| 183 |
-
contact_id=
|
| 184 |
-
customer_id=
|
| 185 |
-
first_name=
|
| 186 |
-
last_name=
|
| 187 |
-
title=
|
| 188 |
-
address=
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
|
|
|
| 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
|