Spaces:
Paused
Paused
Commit ·
f3c922d
1
Parent(s): 86f4f77
feat: Implement distributor management functionality with CRUD operations and pagination
Browse files- app/app.py +1 -0
- app/controllers/distributors.py +300 -0
- app/db/models/distributor.py +26 -0
- app/db/models/reference.py +2 -1
- app/db/repositories/distributor_repo.py +255 -0
- app/db/repositories/reference_repo.py +16 -3
- app/schemas/distributor.py +47 -0
- app/services/distributor_service.py +161 -0
app/app.py
CHANGED
|
@@ -11,6 +11,7 @@ from app.controllers.reports import router as reports_router
|
|
| 11 |
from app.controllers.employees import router as employees_router
|
| 12 |
from app.controllers.reference import router as reference_router
|
| 13 |
from app.controllers.bidders import router as bidders_router
|
|
|
|
| 14 |
|
| 15 |
setup_logging(settings.LOG_LEVEL)
|
| 16 |
|
|
|
|
| 11 |
from app.controllers.employees import router as employees_router
|
| 12 |
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 |
|
| 16 |
setup_logging(settings.LOG_LEVEL)
|
| 17 |
|
app/controllers/distributors.py
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.distributor_service import DistributorService
|
| 5 |
+
from app.schemas.distributor import DistributorCreate, DistributorOut
|
| 6 |
+
from app.schemas.paginated_response import PaginatedResponse
|
| 7 |
+
from typing import Optional, List
|
| 8 |
+
import logging
|
| 9 |
+
|
| 10 |
+
logger = logging.getLogger(__name__)
|
| 11 |
+
|
| 12 |
+
# Common query parameter descriptions
|
| 13 |
+
PAGE_DESC = "Page number (1-indexed)"
|
| 14 |
+
PAGE_SIZE_DESC = "Number of items per page"
|
| 15 |
+
ORDER_BY_DESC = "Field to order by"
|
| 16 |
+
ORDER_DIR_DESC = "Order direction (asc|desc)"
|
| 17 |
+
TERRITORY_DESC = "Territory/Region name"
|
| 18 |
+
STATUS_DESC = "Distributor status (active|inactive|pending)"
|
| 19 |
+
|
| 20 |
+
router = APIRouter(prefix="/api/v1/distributors", tags=["distributors"])
|
| 21 |
+
|
| 22 |
+
@router.get(
|
| 23 |
+
"/",
|
| 24 |
+
response_model=PaginatedResponse[DistributorOut],
|
| 25 |
+
summary="List all distributors",
|
| 26 |
+
response_description="Paginated list of distributors"
|
| 27 |
+
)
|
| 28 |
+
def list_distributors(
|
| 29 |
+
page: int = Query(1, ge=1, description=PAGE_DESC),
|
| 30 |
+
page_size: int = Query(10, ge=1, le=100, description=PAGE_SIZE_DESC),
|
| 31 |
+
order_by: Optional[str] = Query("Id", description=ORDER_BY_DESC),
|
| 32 |
+
order_dir: Optional[str] = Query("asc", description=ORDER_DIR_DESC),
|
| 33 |
+
db: Session = Depends(get_db)
|
| 34 |
+
):
|
| 35 |
+
"""Get all distributors with pagination and ordering"""
|
| 36 |
+
try:
|
| 37 |
+
logger.info(f"Listing distributors: page={page}, page_size={page_size}")
|
| 38 |
+
distributor_service = DistributorService(db)
|
| 39 |
+
result = distributor_service.list(
|
| 40 |
+
page=page,
|
| 41 |
+
page_size=page_size,
|
| 42 |
+
order_by=order_by,
|
| 43 |
+
order_direction=order_dir.upper()
|
| 44 |
+
)
|
| 45 |
+
logger.info(f"Successfully retrieved {len(result.items)} distributors")
|
| 46 |
+
return result
|
| 47 |
+
except Exception as e:
|
| 48 |
+
logger.error(f"Error listing distributors: {e}")
|
| 49 |
+
return PaginatedResponse[DistributorOut](
|
| 50 |
+
items=[],
|
| 51 |
+
page=page,
|
| 52 |
+
page_size=page_size,
|
| 53 |
+
total=0
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
@router.get(
|
| 57 |
+
"/territory/{territory}",
|
| 58 |
+
response_model=PaginatedResponse[DistributorOut],
|
| 59 |
+
summary="List distributors by territory",
|
| 60 |
+
response_description="Paginated list of distributors for a specific territory"
|
| 61 |
+
)
|
| 62 |
+
def list_distributors_by_territory(
|
| 63 |
+
territory: str,
|
| 64 |
+
page: int = Query(1, ge=1, description=PAGE_DESC),
|
| 65 |
+
page_size: int = Query(10, ge=1, le=100, description=PAGE_SIZE_DESC),
|
| 66 |
+
order_by: Optional[str] = Query("Id", description=ORDER_BY_DESC),
|
| 67 |
+
order_dir: Optional[str] = Query("asc", description=ORDER_DIR_DESC),
|
| 68 |
+
db: Session = Depends(get_db)
|
| 69 |
+
):
|
| 70 |
+
"""Get all distributors for a specific territory with pagination and ordering"""
|
| 71 |
+
try:
|
| 72 |
+
logger.info(f"Listing distributors for territory {territory}: page={page}, page_size={page_size}")
|
| 73 |
+
distributor_service = DistributorService(db)
|
| 74 |
+
result = distributor_service.get_by_territory(
|
| 75 |
+
territory=territory,
|
| 76 |
+
page=page,
|
| 77 |
+
page_size=page_size,
|
| 78 |
+
order_by=order_by,
|
| 79 |
+
order_dir=order_dir.upper()
|
| 80 |
+
)
|
| 81 |
+
logger.info(f"Successfully retrieved {len(result.items)} distributors for territory {territory}")
|
| 82 |
+
return result
|
| 83 |
+
except Exception as e:
|
| 84 |
+
logger.error(f"Error listing distributors for territory {territory}: {e}")
|
| 85 |
+
return PaginatedResponse[DistributorOut](
|
| 86 |
+
items=[],
|
| 87 |
+
page=page,
|
| 88 |
+
page_size=page_size,
|
| 89 |
+
total=0
|
| 90 |
+
)
|
| 91 |
+
|
| 92 |
+
@router.get(
|
| 93 |
+
"/status/{status}",
|
| 94 |
+
response_model=PaginatedResponse[DistributorOut],
|
| 95 |
+
summary="List distributors by status",
|
| 96 |
+
response_description="Paginated list of distributors with a specific status"
|
| 97 |
+
)
|
| 98 |
+
def list_distributors_by_status(
|
| 99 |
+
status: str,
|
| 100 |
+
page: int = Query(1, ge=1, description=PAGE_DESC),
|
| 101 |
+
page_size: int = Query(10, ge=1, le=100, description=PAGE_SIZE_DESC),
|
| 102 |
+
order_by: Optional[str] = Query("Id", description=ORDER_BY_DESC),
|
| 103 |
+
order_dir: Optional[str] = Query("asc", description=ORDER_DIR_DESC),
|
| 104 |
+
db: Session = Depends(get_db)
|
| 105 |
+
):
|
| 106 |
+
"""Get all distributors with a specific status with pagination and ordering"""
|
| 107 |
+
try:
|
| 108 |
+
logger.info(f"Listing distributors with status {status}: page={page}, page_size={page_size}")
|
| 109 |
+
distributor_service = DistributorService(db)
|
| 110 |
+
result = distributor_service.get_by_status(
|
| 111 |
+
status=status,
|
| 112 |
+
page=page,
|
| 113 |
+
page_size=page_size,
|
| 114 |
+
order_by=order_by,
|
| 115 |
+
order_dir=order_dir.upper()
|
| 116 |
+
)
|
| 117 |
+
logger.info(f"Successfully retrieved {len(result.items)} distributors with status {status}")
|
| 118 |
+
return result
|
| 119 |
+
except Exception as e:
|
| 120 |
+
logger.error(f"Error listing distributors with status {status}: {e}")
|
| 121 |
+
return PaginatedResponse[DistributorOut](
|
| 122 |
+
items=[],
|
| 123 |
+
page=page,
|
| 124 |
+
page_size=page_size,
|
| 125 |
+
total=0
|
| 126 |
+
)
|
| 127 |
+
|
| 128 |
+
@router.get(
|
| 129 |
+
"/territories",
|
| 130 |
+
response_model=List[str],
|
| 131 |
+
summary="Get all territories",
|
| 132 |
+
response_description="List of unique territories"
|
| 133 |
+
)
|
| 134 |
+
def get_territories(db: Session = Depends(get_db)):
|
| 135 |
+
"""Get all unique territories"""
|
| 136 |
+
try:
|
| 137 |
+
distributor_service = DistributorService(db)
|
| 138 |
+
territories = distributor_service.get_territories()
|
| 139 |
+
logger.info(f"Successfully retrieved {len(territories)} territories")
|
| 140 |
+
return territories
|
| 141 |
+
except Exception as e:
|
| 142 |
+
logger.error(f"Error getting territories: {e}")
|
| 143 |
+
return []
|
| 144 |
+
|
| 145 |
+
@router.get(
|
| 146 |
+
"/{distributor_id}",
|
| 147 |
+
response_model=DistributorOut,
|
| 148 |
+
summary="Get a distributor by ID",
|
| 149 |
+
response_description="Distributor details"
|
| 150 |
+
)
|
| 151 |
+
def get_distributor(distributor_id: int, db: Session = Depends(get_db)):
|
| 152 |
+
"""Get a specific distributor by ID"""
|
| 153 |
+
try:
|
| 154 |
+
distributor_service = DistributorService(db)
|
| 155 |
+
return distributor_service.get(distributor_id)
|
| 156 |
+
except Exception as e:
|
| 157 |
+
logger.error(f"Error getting distributor {distributor_id}: {e}")
|
| 158 |
+
raise HTTPException(
|
| 159 |
+
status_code=status.HTTP_404_NOT_FOUND,
|
| 160 |
+
detail=f"Distributor {distributor_id} not found"
|
| 161 |
+
)
|
| 162 |
+
|
| 163 |
+
@router.post(
|
| 164 |
+
"/",
|
| 165 |
+
response_model=DistributorOut,
|
| 166 |
+
status_code=status.HTTP_201_CREATED,
|
| 167 |
+
summary="Create a new distributor",
|
| 168 |
+
response_description="Created distributor details"
|
| 169 |
+
)
|
| 170 |
+
def create_distributor(
|
| 171 |
+
distributor_in: DistributorCreate,
|
| 172 |
+
db: Session = Depends(get_db)
|
| 173 |
+
):
|
| 174 |
+
"""Create a new distributor"""
|
| 175 |
+
try:
|
| 176 |
+
distributor_service = DistributorService(db)
|
| 177 |
+
logger.info(f"Creating new distributor: {distributor_in.company_name}")
|
| 178 |
+
|
| 179 |
+
result = distributor_service.create(distributor_in.dict())
|
| 180 |
+
logger.info(f"Successfully created distributor {result.id}: {result.company_name}")
|
| 181 |
+
return result
|
| 182 |
+
except ValueError as e:
|
| 183 |
+
logger.error(f"Validation error creating distributor: {e}")
|
| 184 |
+
raise HTTPException(
|
| 185 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 186 |
+
detail=str(e)
|
| 187 |
+
)
|
| 188 |
+
except Exception as e:
|
| 189 |
+
logger.error(f"Error creating distributor: {e}")
|
| 190 |
+
raise HTTPException(
|
| 191 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 192 |
+
detail="Failed to create distributor"
|
| 193 |
+
)
|
| 194 |
+
|
| 195 |
+
@router.put(
|
| 196 |
+
"/{distributor_id}",
|
| 197 |
+
response_model=DistributorOut,
|
| 198 |
+
summary="Update a distributor",
|
| 199 |
+
response_description="Updated distributor details"
|
| 200 |
+
)
|
| 201 |
+
def update_distributor(
|
| 202 |
+
distributor_id: int,
|
| 203 |
+
distributor_in: DistributorCreate,
|
| 204 |
+
db: Session = Depends(get_db)
|
| 205 |
+
):
|
| 206 |
+
"""Update an existing distributor"""
|
| 207 |
+
try:
|
| 208 |
+
distributor_service = DistributorService(db)
|
| 209 |
+
logger.info(f"Updating distributor {distributor_id}: {distributor_in.company_name}")
|
| 210 |
+
|
| 211 |
+
result = distributor_service.update(distributor_id, distributor_in.dict())
|
| 212 |
+
logger.info(f"Successfully updated distributor {distributor_id}: {result.company_name}")
|
| 213 |
+
return result
|
| 214 |
+
except ValueError as e:
|
| 215 |
+
logger.error(f"Validation error updating distributor {distributor_id}: {e}")
|
| 216 |
+
raise HTTPException(
|
| 217 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 218 |
+
detail=str(e)
|
| 219 |
+
)
|
| 220 |
+
except Exception as e:
|
| 221 |
+
logger.error(f"Error updating distributor {distributor_id}: {e}")
|
| 222 |
+
raise HTTPException(
|
| 223 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 224 |
+
detail="Failed to update distributor"
|
| 225 |
+
)
|
| 226 |
+
|
| 227 |
+
@router.delete(
|
| 228 |
+
"/{distributor_id}",
|
| 229 |
+
status_code=status.HTTP_204_NO_CONTENT,
|
| 230 |
+
summary="Delete a distributor",
|
| 231 |
+
response_description="Distributor deleted successfully"
|
| 232 |
+
)
|
| 233 |
+
def delete_distributor(
|
| 234 |
+
distributor_id: int,
|
| 235 |
+
db: Session = Depends(get_db)
|
| 236 |
+
):
|
| 237 |
+
"""Delete a distributor"""
|
| 238 |
+
try:
|
| 239 |
+
distributor_service = DistributorService(db)
|
| 240 |
+
logger.info(f"Deleting distributor {distributor_id}")
|
| 241 |
+
|
| 242 |
+
distributor_service.delete(distributor_id)
|
| 243 |
+
logger.info(f"Successfully deleted distributor {distributor_id}")
|
| 244 |
+
return None
|
| 245 |
+
except Exception as e:
|
| 246 |
+
logger.error(f"Error deleting distributor {distributor_id}: {e}")
|
| 247 |
+
raise HTTPException(
|
| 248 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 249 |
+
detail="Failed to delete distributor"
|
| 250 |
+
)
|
| 251 |
+
|
| 252 |
+
@router.post(
|
| 253 |
+
"/{distributor_id}/activate",
|
| 254 |
+
response_model=DistributorOut,
|
| 255 |
+
summary="Activate a distributor",
|
| 256 |
+
response_description="Activated distributor details"
|
| 257 |
+
)
|
| 258 |
+
def activate_distributor(
|
| 259 |
+
distributor_id: int,
|
| 260 |
+
db: Session = Depends(get_db)
|
| 261 |
+
):
|
| 262 |
+
"""Activate a distributor (set status to active)"""
|
| 263 |
+
try:
|
| 264 |
+
distributor_service = DistributorService(db)
|
| 265 |
+
logger.info(f"Activating distributor {distributor_id}")
|
| 266 |
+
|
| 267 |
+
result = distributor_service.activate(distributor_id)
|
| 268 |
+
logger.info(f"Successfully activated distributor {distributor_id}")
|
| 269 |
+
return result
|
| 270 |
+
except Exception as e:
|
| 271 |
+
logger.error(f"Error activating distributor {distributor_id}: {e}")
|
| 272 |
+
raise HTTPException(
|
| 273 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 274 |
+
detail="Failed to activate distributor"
|
| 275 |
+
)
|
| 276 |
+
|
| 277 |
+
@router.post(
|
| 278 |
+
"/{distributor_id}/deactivate",
|
| 279 |
+
response_model=DistributorOut,
|
| 280 |
+
summary="Deactivate a distributor",
|
| 281 |
+
response_description="Deactivated distributor details"
|
| 282 |
+
)
|
| 283 |
+
def deactivate_distributor(
|
| 284 |
+
distributor_id: int,
|
| 285 |
+
db: Session = Depends(get_db)
|
| 286 |
+
):
|
| 287 |
+
"""Deactivate a distributor (set status to inactive)"""
|
| 288 |
+
try:
|
| 289 |
+
distributor_service = DistributorService(db)
|
| 290 |
+
logger.info(f"Deactivating distributor {distributor_id}")
|
| 291 |
+
|
| 292 |
+
result = distributor_service.deactivate(distributor_id)
|
| 293 |
+
logger.info(f"Successfully deactivated distributor {distributor_id}")
|
| 294 |
+
return result
|
| 295 |
+
except Exception as e:
|
| 296 |
+
logger.error(f"Error deactivating distributor {distributor_id}: {e}")
|
| 297 |
+
raise HTTPException(
|
| 298 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 299 |
+
detail="Failed to deactivate distributor"
|
| 300 |
+
)
|
app/db/models/distributor.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sqlalchemy import Column, Integer, String, Boolean, Float, DateTime, ForeignKey
|
| 2 |
+
from app.db.base import Base
|
| 3 |
+
|
| 4 |
+
class Distributor(Base):
|
| 5 |
+
__tablename__ = "Distributors"
|
| 6 |
+
|
| 7 |
+
Id = Column(Integer, primary_key=True, index=True)
|
| 8 |
+
CompanyName = Column(String(200), nullable=False)
|
| 9 |
+
ContactPerson = Column(String(200), nullable=True)
|
| 10 |
+
EmailAddress = Column(String(255), nullable=True)
|
| 11 |
+
Phone = Column(String(50), nullable=True)
|
| 12 |
+
Fax = Column(String(50), nullable=True)
|
| 13 |
+
Address = Column(String(500), nullable=True)
|
| 14 |
+
City = Column(String(100), nullable=True)
|
| 15 |
+
State = Column(String(50), nullable=True)
|
| 16 |
+
ZipCode = Column(String(20), nullable=True)
|
| 17 |
+
Country = Column(String(100), nullable=True)
|
| 18 |
+
Website = Column(String(255), nullable=True)
|
| 19 |
+
Territory = Column(String(100), nullable=True)
|
| 20 |
+
CommissionRate = Column(Float, nullable=True, default=0.0)
|
| 21 |
+
Status = Column(String(20), nullable=False, default='active')
|
| 22 |
+
Notes = Column(String(1000), nullable=True)
|
| 23 |
+
DateCreated = Column(DateTime, nullable=True)
|
| 24 |
+
DateModified = Column(DateTime, nullable=True)
|
| 25 |
+
Enabled = Column(Boolean, nullable=False, default=True)
|
| 26 |
+
EmployeeId = Column(Integer, nullable=True)
|
app/db/models/reference.py
CHANGED
|
@@ -5,7 +5,8 @@ class State(Base):
|
|
| 5 |
__tablename__ = "States"
|
| 6 |
|
| 7 |
state_id = Column("StateID", Integer, primary_key=True, index=True)
|
| 8 |
-
|
|
|
|
| 9 |
state_code = Column("StateCode", String(2), nullable=False)
|
| 10 |
country = Column("Country", String(50), nullable=True)
|
| 11 |
|
|
|
|
| 5 |
__tablename__ = "States"
|
| 6 |
|
| 7 |
state_id = Column("StateID", Integer, primary_key=True, index=True)
|
| 8 |
+
# Renamed from StateName to State as per database schema
|
| 9 |
+
state_name = Column("State", String(50), nullable=False)
|
| 10 |
state_code = Column("StateCode", String(2), nullable=False)
|
| 11 |
country = Column("Country", String(50), nullable=True)
|
| 12 |
|
app/db/repositories/distributor_repo.py
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sqlalchemy.orm import Session
|
| 2 |
+
from sqlalchemy.exc import SQLAlchemyError
|
| 3 |
+
from app.db.models.distributor import Distributor
|
| 4 |
+
from app.schemas.distributor import DistributorCreate, DistributorOut
|
| 5 |
+
from app.schemas.paginated_response import PaginatedResponse
|
| 6 |
+
from app.core.exceptions import NotFoundException
|
| 7 |
+
from typing import List, Optional
|
| 8 |
+
from datetime import datetime, timezone
|
| 9 |
+
import logging
|
| 10 |
+
|
| 11 |
+
logger = logging.getLogger(__name__)
|
| 12 |
+
|
| 13 |
+
class DistributorRepository:
|
| 14 |
+
def __init__(self, db: Session):
|
| 15 |
+
self.db = db
|
| 16 |
+
|
| 17 |
+
def get(self, distributor_id: int) -> Optional[DistributorOut]:
|
| 18 |
+
"""Get a single distributor by ID using Distributors table directly."""
|
| 19 |
+
try:
|
| 20 |
+
distributor = self.db.query(Distributor).filter(Distributor.Id == distributor_id).first()
|
| 21 |
+
if distributor:
|
| 22 |
+
return self._map_distributor_data(distributor.__dict__)
|
| 23 |
+
return None
|
| 24 |
+
except SQLAlchemyError as e:
|
| 25 |
+
logger.error(f"Database error getting distributor {distributor_id}: {e}")
|
| 26 |
+
raise
|
| 27 |
+
except Exception as e:
|
| 28 |
+
logger.error(f"Unexpected error getting distributor {distributor_id}: {e}")
|
| 29 |
+
raise
|
| 30 |
+
|
| 31 |
+
def list(self, page: int = 1, page_size: int = 10, order_by: str = "Id", order_direction: str = "ASC") -> PaginatedResponse[DistributorOut]:
|
| 32 |
+
"""Get paginated list of all distributors using Distributors table directly with order by."""
|
| 33 |
+
try:
|
| 34 |
+
query = self.db.query(Distributor)
|
| 35 |
+
# Validate order_by field
|
| 36 |
+
if not hasattr(Distributor, order_by):
|
| 37 |
+
order_by = "Id"
|
| 38 |
+
order_col = getattr(Distributor, order_by)
|
| 39 |
+
if order_direction.upper() == "DESC":
|
| 40 |
+
order_col = order_col.desc()
|
| 41 |
+
else:
|
| 42 |
+
order_col = order_col.asc()
|
| 43 |
+
query = query.order_by(order_col)
|
| 44 |
+
total_records = query.count()
|
| 45 |
+
distributors = query.offset((page - 1) * page_size).limit(page_size).all()
|
| 46 |
+
distributor_out_list = [self._map_distributor_data(distributor.__dict__) for distributor in distributors]
|
| 47 |
+
return PaginatedResponse[DistributorOut](
|
| 48 |
+
items=distributor_out_list,
|
| 49 |
+
page=page,
|
| 50 |
+
page_size=page_size,
|
| 51 |
+
total=total_records
|
| 52 |
+
)
|
| 53 |
+
except Exception as e:
|
| 54 |
+
logger.error(f"Error listing distributors: {e}")
|
| 55 |
+
return PaginatedResponse[DistributorOut](
|
| 56 |
+
items=[],
|
| 57 |
+
page=page,
|
| 58 |
+
page_size=page_size,
|
| 59 |
+
total=0
|
| 60 |
+
)
|
| 61 |
+
|
| 62 |
+
def get_by_territory(self, territory: str, page: int = 1, page_size: int = 10,
|
| 63 |
+
order_by: str = "Id", order_dir: str = "ASC") -> PaginatedResponse[DistributorOut]:
|
| 64 |
+
"""Get distributors for a specific territory using Distributors table directly."""
|
| 65 |
+
try:
|
| 66 |
+
query = self.db.query(Distributor).filter(Distributor.Territory == territory)
|
| 67 |
+
# Validate order_by field
|
| 68 |
+
if not hasattr(Distributor, order_by):
|
| 69 |
+
order_by = "Id"
|
| 70 |
+
order_col = getattr(Distributor, order_by)
|
| 71 |
+
if order_dir.upper() == "DESC":
|
| 72 |
+
order_col = order_col.desc()
|
| 73 |
+
else:
|
| 74 |
+
order_col = order_col.asc()
|
| 75 |
+
query = query.order_by(order_col)
|
| 76 |
+
total_records = query.count()
|
| 77 |
+
distributors = query.offset((page - 1) * page_size).limit(page_size).all()
|
| 78 |
+
distributor_out_list = [self._map_distributor_data(distributor.__dict__) for distributor in distributors]
|
| 79 |
+
return PaginatedResponse[DistributorOut](
|
| 80 |
+
items=distributor_out_list,
|
| 81 |
+
page=page,
|
| 82 |
+
page_size=page_size,
|
| 83 |
+
total=total_records
|
| 84 |
+
)
|
| 85 |
+
except Exception as e:
|
| 86 |
+
logger.error(f"Error getting distributors for territory {territory}: {e}")
|
| 87 |
+
return PaginatedResponse[DistributorOut](
|
| 88 |
+
items=[],
|
| 89 |
+
page=page,
|
| 90 |
+
page_size=page_size,
|
| 91 |
+
total=0
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
def get_by_status(self, status: str, page: int = 1, page_size: int = 10,
|
| 95 |
+
order_by: str = "Id", order_dir: str = "ASC") -> PaginatedResponse[DistributorOut]:
|
| 96 |
+
"""Get distributors by status using Distributors table directly."""
|
| 97 |
+
try:
|
| 98 |
+
query = self.db.query(Distributor).filter(Distributor.Status == status)
|
| 99 |
+
# Validate order_by field
|
| 100 |
+
if not hasattr(Distributor, order_by):
|
| 101 |
+
order_by = "Id"
|
| 102 |
+
order_col = getattr(Distributor, order_by)
|
| 103 |
+
if order_dir.upper() == "DESC":
|
| 104 |
+
order_col = order_col.desc()
|
| 105 |
+
else:
|
| 106 |
+
order_col = order_col.asc()
|
| 107 |
+
query = query.order_by(order_col)
|
| 108 |
+
total_records = query.count()
|
| 109 |
+
distributors = query.offset((page - 1) * page_size).limit(page_size).all()
|
| 110 |
+
distributor_out_list = [self._map_distributor_data(distributor.__dict__) for distributor in distributors]
|
| 111 |
+
return PaginatedResponse[DistributorOut](
|
| 112 |
+
items=distributor_out_list,
|
| 113 |
+
page=page,
|
| 114 |
+
page_size=page_size,
|
| 115 |
+
total=total_records
|
| 116 |
+
)
|
| 117 |
+
except Exception as e:
|
| 118 |
+
logger.error(f"Error getting distributors by status {status}: {e}")
|
| 119 |
+
return PaginatedResponse[DistributorOut](
|
| 120 |
+
items=[],
|
| 121 |
+
page=page,
|
| 122 |
+
page_size=page_size,
|
| 123 |
+
total=0
|
| 124 |
+
)
|
| 125 |
+
|
| 126 |
+
def create(self, distributor_data: DistributorCreate) -> DistributorOut:
|
| 127 |
+
"""Create a new distributor using Distributors table directly."""
|
| 128 |
+
try:
|
| 129 |
+
current_time = datetime.now(timezone.utc)
|
| 130 |
+
new_distributor = Distributor(
|
| 131 |
+
CompanyName=distributor_data.company_name,
|
| 132 |
+
ContactPerson=distributor_data.contact_person,
|
| 133 |
+
EmailAddress=distributor_data.email_address,
|
| 134 |
+
Phone=distributor_data.phone,
|
| 135 |
+
Fax=distributor_data.fax,
|
| 136 |
+
Address=distributor_data.address,
|
| 137 |
+
City=distributor_data.city,
|
| 138 |
+
State=distributor_data.state,
|
| 139 |
+
ZipCode=distributor_data.zip_code,
|
| 140 |
+
Country=distributor_data.country,
|
| 141 |
+
Website=distributor_data.website,
|
| 142 |
+
Territory=distributor_data.territory,
|
| 143 |
+
CommissionRate=distributor_data.commission_rate or 0.0,
|
| 144 |
+
Status=distributor_data.status or 'active',
|
| 145 |
+
Notes=distributor_data.notes,
|
| 146 |
+
DateCreated=current_time,
|
| 147 |
+
DateModified=current_time,
|
| 148 |
+
Enabled=distributor_data.enabled or True,
|
| 149 |
+
EmployeeId=distributor_data.employee_id
|
| 150 |
+
)
|
| 151 |
+
self.db.add(new_distributor)
|
| 152 |
+
self.db.commit()
|
| 153 |
+
self.db.refresh(new_distributor)
|
| 154 |
+
return self._map_distributor_data(new_distributor.__dict__)
|
| 155 |
+
except SQLAlchemyError as e:
|
| 156 |
+
logger.error(f"Database error creating distributor: {e}")
|
| 157 |
+
self.db.rollback()
|
| 158 |
+
raise
|
| 159 |
+
except Exception as e:
|
| 160 |
+
logger.error(f"Unexpected error creating distributor: {e}")
|
| 161 |
+
self.db.rollback()
|
| 162 |
+
raise
|
| 163 |
+
|
| 164 |
+
def update(self, distributor_id: int, distributor_data: DistributorCreate) -> DistributorOut:
|
| 165 |
+
"""Update a distributor using Distributors table directly."""
|
| 166 |
+
try:
|
| 167 |
+
distributor = self.db.query(Distributor).filter(Distributor.Id == distributor_id).first()
|
| 168 |
+
if not distributor:
|
| 169 |
+
raise NotFoundException("Distributor not found for update")
|
| 170 |
+
|
| 171 |
+
distributor.CompanyName = distributor_data.company_name
|
| 172 |
+
distributor.ContactPerson = distributor_data.contact_person
|
| 173 |
+
distributor.EmailAddress = distributor_data.email_address
|
| 174 |
+
distributor.Phone = distributor_data.phone
|
| 175 |
+
distributor.Fax = distributor_data.fax
|
| 176 |
+
distributor.Address = distributor_data.address
|
| 177 |
+
distributor.City = distributor_data.city
|
| 178 |
+
distributor.State = distributor_data.state
|
| 179 |
+
distributor.ZipCode = distributor_data.zip_code
|
| 180 |
+
distributor.Country = distributor_data.country
|
| 181 |
+
distributor.Website = distributor_data.website
|
| 182 |
+
distributor.Territory = distributor_data.territory
|
| 183 |
+
distributor.CommissionRate = distributor_data.commission_rate
|
| 184 |
+
distributor.Status = distributor_data.status
|
| 185 |
+
distributor.Notes = distributor_data.notes
|
| 186 |
+
distributor.DateModified = datetime.now(timezone.utc)
|
| 187 |
+
distributor.Enabled = distributor_data.enabled
|
| 188 |
+
distributor.EmployeeId = distributor_data.employee_id
|
| 189 |
+
|
| 190 |
+
self.db.commit()
|
| 191 |
+
self.db.refresh(distributor)
|
| 192 |
+
return self._map_distributor_data(distributor.__dict__)
|
| 193 |
+
except SQLAlchemyError as e:
|
| 194 |
+
logger.error(f"Database error updating distributor {distributor_id}: {e}")
|
| 195 |
+
self.db.rollback()
|
| 196 |
+
raise
|
| 197 |
+
except Exception as e:
|
| 198 |
+
logger.error(f"Unexpected error updating distributor {distributor_id}: {e}")
|
| 199 |
+
self.db.rollback()
|
| 200 |
+
raise
|
| 201 |
+
|
| 202 |
+
def delete(self, distributor_id: int) -> bool:
|
| 203 |
+
"""Delete a distributor using Distributors table directly."""
|
| 204 |
+
try:
|
| 205 |
+
distributor = self.db.query(Distributor).filter(Distributor.Id == distributor_id).first()
|
| 206 |
+
if not distributor:
|
| 207 |
+
raise NotFoundException("Distributor not found for delete")
|
| 208 |
+
self.db.delete(distributor)
|
| 209 |
+
self.db.commit()
|
| 210 |
+
return True
|
| 211 |
+
except SQLAlchemyError as e:
|
| 212 |
+
logger.error(f"Database error deleting distributor {distributor_id}: {e}")
|
| 213 |
+
self.db.rollback()
|
| 214 |
+
raise
|
| 215 |
+
except Exception as e:
|
| 216 |
+
logger.error(f"Unexpected error deleting distributor {distributor_id}: {e}")
|
| 217 |
+
self.db.rollback()
|
| 218 |
+
raise
|
| 219 |
+
|
| 220 |
+
def get_territories(self) -> List[str]:
|
| 221 |
+
"""Get all unique territories."""
|
| 222 |
+
try:
|
| 223 |
+
territories = self.db.query(Distributor.Territory).filter(
|
| 224 |
+
Distributor.Territory.isnot(None),
|
| 225 |
+
Distributor.Territory != ''
|
| 226 |
+
).distinct().all()
|
| 227 |
+
return [territory[0] for territory in territories]
|
| 228 |
+
except Exception as e:
|
| 229 |
+
logger.error(f"Error getting territories: {e}")
|
| 230 |
+
return []
|
| 231 |
+
|
| 232 |
+
def _map_distributor_data(self, distributor_data: dict) -> DistributorOut:
|
| 233 |
+
"""Map database distributor data to DistributorOut schema"""
|
| 234 |
+
return DistributorOut(
|
| 235 |
+
id=distributor_data.get('Id'),
|
| 236 |
+
company_name=distributor_data.get('CompanyName'),
|
| 237 |
+
contact_person=distributor_data.get('ContactPerson'),
|
| 238 |
+
email_address=distributor_data.get('EmailAddress'),
|
| 239 |
+
phone=distributor_data.get('Phone'),
|
| 240 |
+
fax=distributor_data.get('Fax'),
|
| 241 |
+
address=distributor_data.get('Address'),
|
| 242 |
+
city=distributor_data.get('City'),
|
| 243 |
+
state=distributor_data.get('State'),
|
| 244 |
+
zip_code=distributor_data.get('ZipCode'),
|
| 245 |
+
country=distributor_data.get('Country'),
|
| 246 |
+
website=distributor_data.get('Website'),
|
| 247 |
+
territory=distributor_data.get('Territory'),
|
| 248 |
+
commission_rate=distributor_data.get('CommissionRate'),
|
| 249 |
+
status=distributor_data.get('Status'),
|
| 250 |
+
notes=distributor_data.get('Notes'),
|
| 251 |
+
date_created=distributor_data.get('DateCreated'),
|
| 252 |
+
date_modified=distributor_data.get('DateModified'),
|
| 253 |
+
enabled=distributor_data.get('Enabled'),
|
| 254 |
+
employee_id=distributor_data.get('EmployeeId')
|
| 255 |
+
)
|
app/db/repositories/reference_repo.py
CHANGED
|
@@ -48,13 +48,26 @@ class ReferenceDataRepository:
|
|
| 48 |
result = self.db.execute(sp_query)
|
| 49 |
|
| 50 |
if result.returns_rows:
|
| 51 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
self._cache[cache_key] = states_data
|
| 53 |
logger.info(f"Retrieved {len(states_data)} states via stored procedure")
|
| 54 |
return states_data
|
| 55 |
|
| 56 |
except Exception as e:
|
| 57 |
-
logger.warning(f"Stored procedure failed, using fallback: {e}")
|
| 58 |
|
| 59 |
# Fallback to direct query
|
| 60 |
query = self.db.query(State)
|
|
@@ -66,7 +79,7 @@ class ReferenceDataRepository:
|
|
| 66 |
states_data = [
|
| 67 |
{
|
| 68 |
'state_id': state.state_id,
|
| 69 |
-
'state_name': state.state_name,
|
| 70 |
'state_code': state.state_code,
|
| 71 |
'country': state.country
|
| 72 |
}
|
|
|
|
| 48 |
result = self.db.execute(sp_query)
|
| 49 |
|
| 50 |
if result.returns_rows:
|
| 51 |
+
# Map the stored procedure results to our expected schema
|
| 52 |
+
states_data = []
|
| 53 |
+
for row in result.fetchall():
|
| 54 |
+
row_dict = dict(row._mapping)
|
| 55 |
+
# Adjust for potential naming differences in stored procedure output
|
| 56 |
+
# Map the SP's column names to our expected schema
|
| 57 |
+
state_data = {
|
| 58 |
+
'state_id': row_dict.get('StateID', None),
|
| 59 |
+
'state_name': row_dict.get('State', row_dict.get('StateName', None)),
|
| 60 |
+
'state_code': row_dict.get('StateCode', None),
|
| 61 |
+
'country': row_dict.get('Country', None)
|
| 62 |
+
}
|
| 63 |
+
states_data.append(state_data)
|
| 64 |
+
|
| 65 |
self._cache[cache_key] = states_data
|
| 66 |
logger.info(f"Retrieved {len(states_data)} states via stored procedure")
|
| 67 |
return states_data
|
| 68 |
|
| 69 |
except Exception as e:
|
| 70 |
+
logger.warning(f"Stored procedure failed, using fallback: {e}", exc_info=True)
|
| 71 |
|
| 72 |
# Fallback to direct query
|
| 73 |
query = self.db.query(State)
|
|
|
|
| 79 |
states_data = [
|
| 80 |
{
|
| 81 |
'state_id': state.state_id,
|
| 82 |
+
'state_name': state.state_name, # This will now correctly map from the 'State' column
|
| 83 |
'state_code': state.state_code,
|
| 84 |
'country': state.country
|
| 85 |
}
|
app/schemas/distributor.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import BaseModel, Field
|
| 2 |
+
from typing import Optional
|
| 3 |
+
from datetime import datetime
|
| 4 |
+
|
| 5 |
+
class DistributorCreate(BaseModel):
|
| 6 |
+
company_name: str = Field(..., description="Company name")
|
| 7 |
+
contact_person: Optional[str] = Field(None, description="Contact person name")
|
| 8 |
+
email_address: Optional[str] = Field(None, description="Email address")
|
| 9 |
+
phone: Optional[str] = Field(None, description="Phone number")
|
| 10 |
+
fax: Optional[str] = Field(None, description="Fax number")
|
| 11 |
+
address: Optional[str] = Field(None, description="Street address")
|
| 12 |
+
city: Optional[str] = Field(None, description="City")
|
| 13 |
+
state: Optional[str] = Field(None, description="State/Province")
|
| 14 |
+
zip_code: Optional[str] = Field(None, description="ZIP/Postal code")
|
| 15 |
+
country: Optional[str] = Field(None, description="Country")
|
| 16 |
+
website: Optional[str] = Field(None, description="Website URL")
|
| 17 |
+
territory: Optional[str] = Field(None, description="Territory/Region")
|
| 18 |
+
commission_rate: Optional[float] = Field(0.0, description="Commission rate")
|
| 19 |
+
status: Optional[str] = Field("active", description="Distributor status")
|
| 20 |
+
notes: Optional[str] = Field(None, description="Additional notes")
|
| 21 |
+
enabled: Optional[bool] = Field(True, description="Enabled status")
|
| 22 |
+
employee_id: Optional[int] = Field(None, description="Employee ID")
|
| 23 |
+
|
| 24 |
+
class DistributorOut(BaseModel):
|
| 25 |
+
id: int = Field(..., description="Distributor ID")
|
| 26 |
+
company_name: str = Field(..., description="Company name")
|
| 27 |
+
contact_person: Optional[str] = Field(None, description="Contact person name")
|
| 28 |
+
email_address: Optional[str] = Field(None, description="Email address")
|
| 29 |
+
phone: Optional[str] = Field(None, description="Phone number")
|
| 30 |
+
fax: Optional[str] = Field(None, description="Fax number")
|
| 31 |
+
address: Optional[str] = Field(None, description="Street address")
|
| 32 |
+
city: Optional[str] = Field(None, description="City")
|
| 33 |
+
state: Optional[str] = Field(None, description="State/Province")
|
| 34 |
+
zip_code: Optional[str] = Field(None, description="ZIP/Postal code")
|
| 35 |
+
country: Optional[str] = Field(None, description="Country")
|
| 36 |
+
website: Optional[str] = Field(None, description="Website URL")
|
| 37 |
+
territory: Optional[str] = Field(None, description="Territory/Region")
|
| 38 |
+
commission_rate: Optional[float] = Field(None, description="Commission rate")
|
| 39 |
+
status: Optional[str] = Field(None, description="Distributor status")
|
| 40 |
+
notes: Optional[str] = Field(None, description="Additional notes")
|
| 41 |
+
date_created: Optional[datetime] = Field(None, description="Date created")
|
| 42 |
+
date_modified: Optional[datetime] = Field(None, description="Date modified")
|
| 43 |
+
enabled: Optional[bool] = Field(None, description="Enabled status")
|
| 44 |
+
employee_id: Optional[int] = Field(None, description="Employee ID")
|
| 45 |
+
|
| 46 |
+
class Config:
|
| 47 |
+
from_attributes = True # Updated for Pydantic v2
|
app/services/distributor_service.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sqlalchemy.orm import Session
|
| 2 |
+
from app.db.repositories.distributor_repo import DistributorRepository
|
| 3 |
+
from app.schemas.distributor import DistributorCreate, DistributorOut
|
| 4 |
+
from app.schemas.paginated_response import PaginatedResponse
|
| 5 |
+
from app.core.exceptions import NotFoundException
|
| 6 |
+
from typing import List, Optional
|
| 7 |
+
|
| 8 |
+
# Constants
|
| 9 |
+
DISTRIBUTOR_NOT_FOUND_MSG = "Distributor not found"
|
| 10 |
+
|
| 11 |
+
class DistributorService:
|
| 12 |
+
def __init__(self, db: Session):
|
| 13 |
+
self.repo = DistributorRepository(db)
|
| 14 |
+
|
| 15 |
+
def get(self, distributor_id: int) -> DistributorOut:
|
| 16 |
+
"""Get a single distributor by ID"""
|
| 17 |
+
distributor = self.repo.get(distributor_id)
|
| 18 |
+
if not distributor:
|
| 19 |
+
raise NotFoundException(DISTRIBUTOR_NOT_FOUND_MSG)
|
| 20 |
+
return distributor
|
| 21 |
+
|
| 22 |
+
def list(self, page: int = 1, page_size: int = 10, order_by: str = "Id",
|
| 23 |
+
order_direction: str = "ASC") -> PaginatedResponse[DistributorOut]:
|
| 24 |
+
"""Get a paginated list of all distributors"""
|
| 25 |
+
return self.repo.list(page, page_size, order_by, order_direction)
|
| 26 |
+
|
| 27 |
+
def get_by_territory(self, territory: str, page: int = 1, page_size: int = 10,
|
| 28 |
+
order_by: str = "Id", order_dir: str = "ASC") -> PaginatedResponse[DistributorOut]:
|
| 29 |
+
"""Get all distributors for a specific territory"""
|
| 30 |
+
return self.repo.get_by_territory(territory, page, page_size, order_by, order_dir)
|
| 31 |
+
|
| 32 |
+
def get_by_status(self, status: str, page: int = 1, page_size: int = 10,
|
| 33 |
+
order_by: str = "Id", order_dir: str = "ASC") -> PaginatedResponse[DistributorOut]:
|
| 34 |
+
"""Get all distributors by status"""
|
| 35 |
+
return self.repo.get_by_status(status, page, page_size, order_by, order_dir)
|
| 36 |
+
|
| 37 |
+
def create(self, data: dict) -> DistributorOut:
|
| 38 |
+
"""Create a new distributor"""
|
| 39 |
+
# Validate and clean data
|
| 40 |
+
validated_data = self._validate_distributor_data(data)
|
| 41 |
+
|
| 42 |
+
# Convert dict to DistributorCreate for validation
|
| 43 |
+
distributor_data = DistributorCreate(**validated_data)
|
| 44 |
+
|
| 45 |
+
return self.repo.create(distributor_data)
|
| 46 |
+
|
| 47 |
+
def update(self, distributor_id: int, data: dict) -> DistributorOut:
|
| 48 |
+
"""Update an existing distributor"""
|
| 49 |
+
# Check if distributor exists
|
| 50 |
+
existing_distributor = self.repo.get(distributor_id)
|
| 51 |
+
if not existing_distributor:
|
| 52 |
+
raise NotFoundException(DISTRIBUTOR_NOT_FOUND_MSG)
|
| 53 |
+
|
| 54 |
+
# Validate and clean data
|
| 55 |
+
validated_data = self._validate_distributor_data(data)
|
| 56 |
+
|
| 57 |
+
# Convert dict to DistributorCreate for validation
|
| 58 |
+
distributor_data = DistributorCreate(**validated_data)
|
| 59 |
+
|
| 60 |
+
return self.repo.update(distributor_id, distributor_data)
|
| 61 |
+
|
| 62 |
+
def delete(self, distributor_id: int):
|
| 63 |
+
"""Delete a distributor"""
|
| 64 |
+
# Check if distributor exists
|
| 65 |
+
existing_distributor = self.repo.get(distributor_id)
|
| 66 |
+
if not existing_distributor:
|
| 67 |
+
raise NotFoundException(DISTRIBUTOR_NOT_FOUND_MSG)
|
| 68 |
+
|
| 69 |
+
self.repo.delete(distributor_id)
|
| 70 |
+
|
| 71 |
+
def get_territories(self) -> List[str]:
|
| 72 |
+
"""Get all unique territories"""
|
| 73 |
+
return self.repo.get_territories()
|
| 74 |
+
|
| 75 |
+
def activate(self, distributor_id: int) -> DistributorOut:
|
| 76 |
+
"""Activate a distributor"""
|
| 77 |
+
# Check if distributor exists
|
| 78 |
+
existing_distributor = self.repo.get(distributor_id)
|
| 79 |
+
if not existing_distributor:
|
| 80 |
+
raise NotFoundException(DISTRIBUTOR_NOT_FOUND_MSG)
|
| 81 |
+
|
| 82 |
+
# Update status to active
|
| 83 |
+
distributor_data = DistributorCreate(**{
|
| 84 |
+
**existing_distributor.dict(),
|
| 85 |
+
'status': 'active'
|
| 86 |
+
})
|
| 87 |
+
|
| 88 |
+
return self.repo.update(distributor_id, distributor_data)
|
| 89 |
+
|
| 90 |
+
def deactivate(self, distributor_id: int) -> DistributorOut:
|
| 91 |
+
"""Deactivate a distributor"""
|
| 92 |
+
# Check if distributor exists
|
| 93 |
+
existing_distributor = self.repo.get(distributor_id)
|
| 94 |
+
if not existing_distributor:
|
| 95 |
+
raise NotFoundException(DISTRIBUTOR_NOT_FOUND_MSG)
|
| 96 |
+
|
| 97 |
+
# Update status to inactive
|
| 98 |
+
distributor_data = DistributorCreate(**{
|
| 99 |
+
**existing_distributor.dict(),
|
| 100 |
+
'status': 'inactive'
|
| 101 |
+
})
|
| 102 |
+
|
| 103 |
+
return self.repo.update(distributor_id, distributor_data)
|
| 104 |
+
|
| 105 |
+
def _validate_distributor_data(self, data: dict) -> dict:
|
| 106 |
+
"""Internal method to validate and clean distributor data"""
|
| 107 |
+
# Ensure company name is provided (required)
|
| 108 |
+
if not data.get('company_name'):
|
| 109 |
+
raise ValueError("Company name is required")
|
| 110 |
+
|
| 111 |
+
# Clean and validate email
|
| 112 |
+
self._validate_email(data)
|
| 113 |
+
|
| 114 |
+
# Clean phone numbers
|
| 115 |
+
self._clean_phone_fields(data)
|
| 116 |
+
|
| 117 |
+
# Validate commission rate
|
| 118 |
+
self._validate_commission_rate(data)
|
| 119 |
+
|
| 120 |
+
# Validate status
|
| 121 |
+
self._validate_status(data)
|
| 122 |
+
|
| 123 |
+
# Clean website URL
|
| 124 |
+
self._clean_website_url(data)
|
| 125 |
+
|
| 126 |
+
return data
|
| 127 |
+
|
| 128 |
+
def _validate_email(self, data: dict):
|
| 129 |
+
"""Validate and clean email address"""
|
| 130 |
+
if data.get('email_address'):
|
| 131 |
+
email = data['email_address'].strip().lower()
|
| 132 |
+
if '@' not in email:
|
| 133 |
+
raise ValueError("Invalid email address format")
|
| 134 |
+
data['email_address'] = email
|
| 135 |
+
|
| 136 |
+
def _clean_phone_fields(self, data: dict):
|
| 137 |
+
"""Clean phone number fields"""
|
| 138 |
+
phone_fields = ['phone', 'fax']
|
| 139 |
+
for field in phone_fields:
|
| 140 |
+
if data.get(field):
|
| 141 |
+
cleaned_phone = ''.join(c for c in data[field] if c.isdigit() or c in '+()-. ')
|
| 142 |
+
data[field] = cleaned_phone.strip()
|
| 143 |
+
|
| 144 |
+
def _validate_commission_rate(self, data: dict):
|
| 145 |
+
"""Validate commission rate"""
|
| 146 |
+
if data.get('commission_rate') is not None and data.get('commission_rate') < 0:
|
| 147 |
+
raise ValueError("Commission rate must be a positive number")
|
| 148 |
+
|
| 149 |
+
def _validate_status(self, data: dict):
|
| 150 |
+
"""Validate status field"""
|
| 151 |
+
valid_statuses = ['active', 'inactive', 'pending']
|
| 152 |
+
if data.get('status') and data.get('status') not in valid_statuses:
|
| 153 |
+
raise ValueError(f"Status must be one of: {', '.join(valid_statuses)}")
|
| 154 |
+
|
| 155 |
+
def _clean_website_url(self, data: dict):
|
| 156 |
+
"""Clean and format website URL"""
|
| 157 |
+
if data.get('website'):
|
| 158 |
+
website = data['website'].strip()
|
| 159 |
+
if website and not website.startswith(('http://', 'https://')):
|
| 160 |
+
website = 'https://' + website
|
| 161 |
+
data['website'] = website
|