MukeshKapoor25 commited on
Commit
1e41209
·
1 Parent(s): ed9094c

feat: Add address management with CRUD operations and integrate into the application

Browse files
app/app.py CHANGED
@@ -13,6 +13,7 @@ from app.controllers.reference import router as reference_router
13
  from app.controllers.bidders import router as bidders_router
14
  from app.controllers.distributors import router as distributors_router
15
  from app.controllers.notes import router as notes_router
 
16
 
17
  setup_logging(settings.LOG_LEVEL)
18
 
@@ -58,3 +59,4 @@ app.include_router(employees_router)
58
  app.include_router(reference_router)
59
  app.include_router(bidders_router)
60
  app.include_router(notes_router)
 
 
13
  from app.controllers.bidders import router as bidders_router
14
  from app.controllers.distributors import router as distributors_router
15
  from app.controllers.notes import router as notes_router
16
+ from app.controllers.addresses import router as addresses_router
17
 
18
  setup_logging(settings.LOG_LEVEL)
19
 
 
59
  app.include_router(reference_router)
60
  app.include_router(bidders_router)
61
  app.include_router(notes_router)
62
+ app.include_router(addresses_router)
app/controllers/addresses.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, status, Query, HTTPException
2
+ from sqlalchemy.orm import Session
3
+ from app.db.session import get_db
4
+ from app.services.address_service import AddressService
5
+ from app.schemas.address import AddressCreate, AddressOut
6
+ from app.schemas.paginated_response import PaginatedResponse
7
+ from typing import Optional
8
+ import logging
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+ router = APIRouter(prefix="/api/v1/addresses", tags=["addresses"])
13
+
14
+
15
+ @router.get(
16
+ "/",
17
+ response_model=PaginatedResponse[AddressOut],
18
+ summary="List addresses",
19
+ response_description="Paginated list of addresses"
20
+ )
21
+ def list_addresses(
22
+ customer_id: Optional[int] = Query(None, description="Optional customer id to filter addresses"),
23
+ page: int = Query(1, ge=1, description="Page number (1-indexed)"),
24
+ page_size: int = Query(10, ge=1, le=100, description="Number of items per page"),
25
+ order_by: Optional[str] = Query("Id", description="Field to order by"),
26
+ order_dir: Optional[str] = Query("asc", description="Order direction (asc|desc)"),
27
+ db: Session = Depends(get_db)
28
+ ):
29
+ try:
30
+ service = AddressService(db)
31
+ result = service.list(page=page, page_size=page_size, order_by=order_by, order_dir=order_dir, customer_id=customer_id)
32
+ return result
33
+ except Exception as e:
34
+ logger.error(f"Error listing addresses: {e}")
35
+ return PaginatedResponse[AddressOut](items=[], page=page, page_size=page_size, total=0)
36
+
37
+
38
+ @router.get("/{address_id}", response_model=AddressOut, summary="Get address by id")
39
+ def get_address(address_id: int, db: Session = Depends(get_db)):
40
+ try:
41
+ service = AddressService(db)
42
+ return service.get(address_id)
43
+ except Exception as e:
44
+ logger.error(f"Error getting address {address_id}: {e}")
45
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Address {address_id} not found")
46
+
47
+
48
+ @router.post("/", response_model=AddressOut, status_code=status.HTTP_201_CREATED)
49
+ def create_address(address_in: AddressCreate, db: Session = Depends(get_db)):
50
+ try:
51
+ service = AddressService(db)
52
+ result = service.create(address_in.dict())
53
+ return result
54
+ except ValueError as e:
55
+ logger.error(f"Validation error creating address: {e}")
56
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
57
+ except Exception as e:
58
+ logger.error(f"Error creating address: {e}")
59
+ raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to create address")
60
+
61
+
62
+ @router.put("/{address_id}", response_model=AddressOut)
63
+ def update_address(address_id: int, address_in: AddressCreate, db: Session = Depends(get_db)):
64
+ try:
65
+ service = AddressService(db)
66
+ result = service.update(address_id, address_in.dict(exclude_unset=True))
67
+ return result
68
+ except ValueError as e:
69
+ logger.error(f"Validation error updating address {address_id}: {e}")
70
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
71
+ except Exception as e:
72
+ logger.error(f"Error updating address {address_id}: {e}")
73
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Address {address_id} not found")
74
+
75
+
76
+ @router.delete("/{address_id}", status_code=status.HTTP_204_NO_CONTENT)
77
+ def delete_address(address_id: int, db: Session = Depends(get_db)):
78
+ try:
79
+ service = AddressService(db)
80
+ service.delete(address_id)
81
+ return None
82
+ except Exception as e:
83
+ logger.error(f"Error deleting address {address_id}: {e}")
84
+ raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to delete address")
app/db/models/address.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy import Column, Integer, String, Boolean
2
+ from app.db.base import Base
3
+
4
+
5
+ class Address(Base):
6
+ __tablename__ = "Address"
7
+
8
+ Id = Column(Integer, primary_key=True, index=True)
9
+ Address = Column(String(200), nullable=True)
10
+ City = Column(String(100), nullable=True)
11
+ StateId = Column(Integer, nullable=True)
12
+ PostalCode = Column(String(70), nullable=True)
13
+ CustomerId = Column(Integer, nullable=True)
14
+ Enabled = Column(Boolean, nullable=False, default=True)
15
+ IsCustomAddress = Column(Boolean, nullable=True, default=False)
16
+ TempAddress = Column(Boolean, nullable=True, default=False)
app/db/repositories/address_repo.py ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy.orm import Session
2
+ from sqlalchemy.exc import SQLAlchemyError
3
+ from app.db.models.address import Address
4
+ from app.schemas.address import AddressCreate, AddressOut
5
+ from app.schemas.paginated_response import PaginatedResponse
6
+ from app.core.exceptions import NotFoundException
7
+ from typing import Optional
8
+ import logging
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class AddressRepository:
14
+ def __init__(self, db: Session):
15
+ self.db = db
16
+
17
+ def get(self, address_id: int) -> Optional[AddressOut]:
18
+ try:
19
+ addr = self.db.query(Address).filter(Address.Id == address_id).first()
20
+ if addr:
21
+ return self._map_address(addr.__dict__)
22
+ return None
23
+ except SQLAlchemyError as e:
24
+ logger.error(f"Database error getting address {address_id}: {e}")
25
+ raise
26
+
27
+ def list(self, page: int = 1, page_size: int = 10, order_by: str = "Id", order_direction: str = "ASC", customer_id: int = None) -> PaginatedResponse[AddressOut]:
28
+ try:
29
+ query = self.db.query(Address)
30
+ if customer_id is not None:
31
+ query = query.filter(Address.CustomerId == customer_id)
32
+
33
+ # validate order_by
34
+ if not hasattr(Address, order_by):
35
+ order_by = "Id"
36
+ order_col = getattr(Address, order_by)
37
+ order_col = order_col.desc() if order_direction.upper() == "DESC" else order_col.asc()
38
+ query = query.order_by(order_col)
39
+
40
+ total = query.count()
41
+ rows = query.offset((page - 1) * page_size).limit(page_size).all()
42
+ items = [self._map_address(r.__dict__) for r in rows]
43
+ return PaginatedResponse[AddressOut](items=items, page=page, page_size=page_size, total=total)
44
+ except Exception as e:
45
+ logger.error(f"Error listing addresses: {e}")
46
+ return PaginatedResponse[AddressOut](items=[], page=page, page_size=page_size, total=0)
47
+
48
+ def create(self, address_data: AddressCreate) -> AddressOut:
49
+ try:
50
+ new_address = Address(
51
+ Address=address_data.address,
52
+ City=address_data.city,
53
+ StateId=address_data.state_id,
54
+ PostalCode=address_data.postal_code,
55
+ CustomerId=address_data.customer_id,
56
+ Enabled=address_data.enabled if address_data.enabled is not None else True,
57
+ IsCustomAddress=address_data.is_custom_address or False,
58
+ TempAddress=address_data.temp_address or False,
59
+ )
60
+ self.db.add(new_address)
61
+ self.db.commit()
62
+ self.db.refresh(new_address)
63
+ return self._map_address(new_address.__dict__)
64
+ except SQLAlchemyError as e:
65
+ logger.error(f"Database error creating address: {e}")
66
+ self.db.rollback()
67
+ raise
68
+
69
+ def update(self, address_id: int, address_data: AddressCreate) -> AddressOut:
70
+ try:
71
+ addr = self.db.query(Address).filter(Address.Id == address_id).first()
72
+ if not addr:
73
+ raise NotFoundException("Address not found for update")
74
+ addr.Address = address_data.address
75
+ addr.City = address_data.city
76
+ addr.StateId = address_data.state_id
77
+ addr.PostalCode = address_data.postal_code
78
+ addr.CustomerId = address_data.customer_id
79
+ addr.Enabled = address_data.enabled if address_data.enabled is not None else addr.Enabled
80
+ addr.IsCustomAddress = address_data.is_custom_address or addr.IsCustomAddress
81
+ addr.TempAddress = address_data.temp_address or addr.TempAddress
82
+ self.db.commit()
83
+ self.db.refresh(addr)
84
+ return self._map_address(addr.__dict__)
85
+ except SQLAlchemyError as e:
86
+ logger.error(f"Database error updating address {address_id}: {e}")
87
+ self.db.rollback()
88
+ raise
89
+
90
+ def delete(self, address_id: int) -> bool:
91
+ try:
92
+ addr = self.db.query(Address).filter(Address.Id == address_id).first()
93
+ if not addr:
94
+ raise NotFoundException("Address not found for delete")
95
+ self.db.delete(addr)
96
+ self.db.commit()
97
+ return True
98
+ except SQLAlchemyError as e:
99
+ logger.error(f"Database error deleting address {address_id}: {e}")
100
+ self.db.rollback()
101
+ raise
102
+
103
+ def _map_address(self, data: dict) -> AddressOut:
104
+ return AddressOut(
105
+ id=data.get('Id'),
106
+ customer_id=data.get('CustomerId'),
107
+ address=data.get('Address'),
108
+ city=data.get('City'),
109
+ state_id=data.get('StateId'),
110
+ postal_code=data.get('PostalCode'),
111
+ enabled=data.get('Enabled'),
112
+ is_custom_address=data.get('IsCustomAddress'),
113
+ temp_address=data.get('TempAddress')
114
+ )
app/schemas/address.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, Field
2
+ from typing import Optional
3
+
4
+
5
+ class AddressCreate(BaseModel):
6
+ customer_id: Optional[int] = Field(None, description="Customer ID this address belongs to")
7
+ address: Optional[str] = Field(None, max_length=200, description="Street address")
8
+ city: Optional[str] = Field(None, max_length=100, description="City")
9
+ state_id: Optional[int] = Field(None, description="State ID")
10
+ postal_code: Optional[str] = Field(None, max_length=70, description="Postal/ZIP code")
11
+ enabled: Optional[bool] = Field(True, description="Enabled flag")
12
+ is_custom_address: Optional[bool] = Field(False, description="Is a custom address")
13
+ temp_address: Optional[bool] = Field(False, description="Temporary address flag")
14
+
15
+
16
+ class AddressOut(BaseModel):
17
+ id: int = Field(..., description="Address ID")
18
+ customer_id: Optional[int] = Field(None, description="Customer ID this address belongs to")
19
+ address: Optional[str] = Field(None, description="Street address")
20
+ city: Optional[str] = Field(None, description="City")
21
+ state_id: Optional[int] = Field(None, description="State ID")
22
+ postal_code: Optional[str] = Field(None, description="Postal/ZIP code")
23
+ enabled: Optional[bool] = Field(None, description="Enabled flag")
24
+ is_custom_address: Optional[bool] = Field(None, description="Is a custom address")
25
+ temp_address: Optional[bool] = Field(None, description="Temporary address flag")
26
+
27
+ class Config:
28
+ from_attributes = True
app/services/address_service.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy.orm import Session
2
+ from app.db.repositories.address_repo import AddressRepository
3
+ from app.schemas.address import AddressCreate, AddressOut
4
+ from app.schemas.paginated_response import PaginatedResponse
5
+ from app.core.exceptions import NotFoundException
6
+ from typing import Optional
7
+
8
+
9
+ class AddressService:
10
+ def __init__(self, db: Session):
11
+ self.repo = AddressRepository(db)
12
+
13
+ def get(self, address_id: int) -> AddressOut:
14
+ addr = self.repo.get(address_id)
15
+ if not addr:
16
+ raise NotFoundException("Address not found")
17
+ return addr
18
+
19
+ def list(self, page: int = 1, page_size: int = 10, order_by: str = "Id", order_dir: str = "ASC", customer_id: Optional[int] = None) -> PaginatedResponse[AddressOut]:
20
+ return self.repo.list(page=page, page_size=page_size, order_by=order_by, order_direction=order_dir, customer_id=customer_id)
21
+
22
+ def create(self, data: dict) -> AddressOut:
23
+ address_data = AddressCreate(**data)
24
+ return self.repo.create(address_data)
25
+
26
+ def update(self, address_id: int, data: dict) -> AddressOut:
27
+ existing = self.repo.get(address_id)
28
+ if not existing:
29
+ raise NotFoundException("Address not found")
30
+ address_data = AddressCreate(**data)
31
+ return self.repo.update(address_id, address_data)
32
+
33
+ def delete(self, address_id: int):
34
+ existing = self.repo.get(address_id)
35
+ if not existing:
36
+ raise NotFoundException("Address not found")
37
+ self.repo.delete(address_id)
tests/unit/conftest.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pytest
2
+ from sqlalchemy import create_engine
3
+ from sqlalchemy.orm import sessionmaker
4
+
5
+ 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
12
+ def db_session():
13
+ """Create a new database session with an in-memory SQLite DB for tests."""
14
+ engine = create_engine("sqlite:///:memory:", connect_args={"check_same_thread": False})
15
+ testing_session_local = sessionmaker(autocommit=False, autoflush=False, bind=engine)
16
+
17
+ # Create tables for all models
18
+ Base.metadata.create_all(bind=engine)
19
+
20
+ db = testing_session_local()
21
+ try:
22
+ yield db
23
+ finally:
24
+ db.close()
tests/unit/test_address_repository.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from app.db.repositories.address_repo import AddressRepository
2
+ from app.schemas.address import AddressCreate
3
+
4
+
5
+ def test_create_get_delete_address(db_session):
6
+ repo = AddressRepository(db_session)
7
+
8
+ # Create
9
+ create_in = AddressCreate(
10
+ customer_id=1,
11
+ address="123 Test St",
12
+ city="Testville",
13
+ state_id=5,
14
+ postal_code="99999"
15
+ )
16
+ created = repo.create(create_in)
17
+ assert created is not None
18
+ assert created.id is not None
19
+ assert created.address == "123 Test St"
20
+
21
+ # Get
22
+ fetched = repo.get(created.id)
23
+ assert fetched is not None
24
+ assert fetched.id == created.id
25
+ assert fetched.city == "Testville"
26
+
27
+ # Delete
28
+ deleted = repo.delete(created.id)
29
+ assert deleted is True
30
+
31
+ # After delete, get should return None
32
+ after = repo.get(created.id)
33
+ assert after is None
tests/unit/test_address_service.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pytest
2
+
3
+ from app.services.address_service import AddressService
4
+ from app.core.exceptions import NotFoundException
5
+
6
+
7
+ def test_service_create_get_update_delete(db_session):
8
+ service = AddressService(db_session)
9
+
10
+ data = {
11
+ "customer_id": 2,
12
+ "address": "50 Service Rd",
13
+ "city": "Servetown",
14
+ "state_id": 7,
15
+ "postal_code": "55555"
16
+ }
17
+
18
+ # Create
19
+ created = service.create(data)
20
+ assert created is not None
21
+ assert created.id is not None
22
+ assert created.customer_id == 2
23
+
24
+ # Get
25
+ fetched = service.get(created.id)
26
+ assert fetched.id == created.id
27
+ assert fetched.address == "50 Service Rd"
28
+
29
+ # Update
30
+ update_data = {"address": "51 Service Rd", "city": "Newtown"}
31
+ updated = service.update(created.id, update_data)
32
+ assert updated.address == "51 Service Rd"
33
+ assert updated.city == "Newtown"
34
+
35
+ # Delete
36
+ service.delete(created.id)
37
+
38
+ # After delete, get should raise NotFoundException
39
+ with pytest.raises(NotFoundException):
40
+ service.get(created.id)