# services/suppliers/app/repositories/supplier_repository.py """ Supplier repository for database operations """ from typing import List, Optional, Dict, Any from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import and_, or_, func, select from uuid import UUID from datetime import datetime from app.models.suppliers import Supplier, SupplierStatus, SupplierType from app.repositories.base import BaseRepository class SupplierRepository(BaseRepository[Supplier]): """Repository for supplier management operations""" def __init__(self, db: AsyncSession): super().__init__(Supplier, db) async def get_by_name(self, tenant_id: UUID, name: str) -> Optional[Supplier]: """Get supplier by name within tenant""" stmt = select(self.model).filter( and_( self.model.tenant_id == tenant_id, self.model.name == name ) ) result = await self.db.execute(stmt) return result.scalar_one_or_none() async def get_by_supplier_code(self, tenant_id: UUID, supplier_code: str) -> Optional[Supplier]: """Get supplier by supplier code within tenant""" stmt = select(self.model).filter( and_( self.model.tenant_id == tenant_id, self.model.supplier_code == supplier_code ) ) result = await self.db.execute(stmt) return result.scalar_one_or_none() async def search_suppliers( self, tenant_id: UUID, search_term: Optional[str] = None, supplier_type: Optional[SupplierType] = None, status: Optional[SupplierStatus] = None, limit: int = 50, offset: int = 0 ) -> List[Supplier]: """Search suppliers with filters""" stmt = select(self.model).filter(self.model.tenant_id == tenant_id) # Search term filter (name, contact person, email) if search_term: search_filter = or_( self.model.name.ilike(f"%{search_term}%"), self.model.contact_person.ilike(f"%{search_term}%"), self.model.email.ilike(f"%{search_term}%") ) stmt = stmt.filter(search_filter) # Type filter if supplier_type: stmt = stmt.filter(self.model.supplier_type == supplier_type) # Status filter if status: stmt = stmt.filter(self.model.status == status) stmt = stmt.order_by(self.model.name).limit(limit).offset(offset) result = await self.db.execute(stmt) return result.scalars().all() async def get_active_suppliers(self, tenant_id: UUID) -> List[Supplier]: """Get all active suppliers for a tenant""" stmt = select(self.model).filter( and_( self.model.tenant_id == tenant_id, self.model.status == SupplierStatus.active ) ).order_by(self.model.name) result = await self.db.execute(stmt) return result.scalars().all() def get_suppliers_by_type( self, tenant_id: UUID, supplier_type: SupplierType ) -> List[Supplier]: """Get suppliers by type""" return ( self.db.query(self.model) .filter( and_( self.model.tenant_id == tenant_id, self.model.supplier_type == supplier_type, self.model.status == SupplierStatus.active ) ) .order_by(self.model.quality_rating.desc(), self.model.name) .all() ) def get_top_suppliers( self, tenant_id: UUID, limit: int = 10 ) -> List[Supplier]: """Get top suppliers by quality rating and order value""" return ( self.db.query(self.model) .filter( and_( self.model.tenant_id == tenant_id, self.model.status == SupplierStatus.active ) ) .order_by( self.model.quality_rating.desc(), self.model.total_amount.desc() ) .limit(limit) .all() ) def update_supplier_stats( self, supplier_id: UUID, total_orders_increment: int = 0, total_amount_increment: float = 0.0, new_quality_rating: Optional[float] = None, new_delivery_rating: Optional[float] = None ) -> Optional[Supplier]: """Update supplier performance statistics""" supplier = self.get_by_id(supplier_id) if not supplier: return None # Update counters if total_orders_increment: supplier.total_orders += total_orders_increment if total_amount_increment: supplier.total_amount += total_amount_increment # Update ratings (these should be calculated averages) if new_quality_rating is not None: supplier.quality_rating = new_quality_rating if new_delivery_rating is not None: supplier.delivery_rating = new_delivery_rating supplier.updated_at = datetime.utcnow() self.db.commit() self.db.refresh(supplier) return supplier def get_suppliers_needing_review( self, tenant_id: UUID, days_since_last_order: int = 30 ) -> List[Supplier]: """Get suppliers that may need performance review""" from datetime import datetime, timedelta cutoff_date = datetime.utcnow() - timedelta(days=days_since_last_order) return ( self.db.query(self.model) .filter( and_( self.model.tenant_id == tenant_id, self.model.status == SupplierStatus.active, or_( self.model.quality_rating < 3.0, # Poor rating self.model.delivery_rating < 3.0, # Poor delivery self.model.updated_at < cutoff_date # Long time since interaction ) ) ) .order_by(self.model.quality_rating.asc()) .all() ) async def get_supplier_statistics(self, tenant_id: UUID) -> Dict[str, Any]: """Get supplier statistics for dashboard""" total_suppliers = await self.count_by_tenant(tenant_id) # Get all suppliers for this tenant to avoid multiple queries and enum casting issues all_stmt = select(self.model).filter(self.model.tenant_id == tenant_id) all_result = await self.db.execute(all_stmt) all_suppliers = all_result.scalars().all() # Calculate statistics in Python to avoid database enum casting issues active_suppliers = [s for s in all_suppliers if s.status == SupplierStatus.active] pending_suppliers = [s for s in all_suppliers if s.status == SupplierStatus.pending_approval] # Calculate averages from active suppliers quality_ratings = [s.quality_rating for s in active_suppliers if s.quality_rating and s.quality_rating > 0] avg_quality_rating = sum(quality_ratings) / len(quality_ratings) if quality_ratings else 0.0 delivery_ratings = [s.delivery_rating for s in active_suppliers if s.delivery_rating and s.delivery_rating > 0] avg_delivery_rating = sum(delivery_ratings) / len(delivery_ratings) if delivery_ratings else 0.0 # Total spend for all suppliers total_spend = sum(float(s.total_amount or 0) for s in all_suppliers) return { "total_suppliers": total_suppliers, "active_suppliers": len(active_suppliers), "pending_suppliers": len(pending_suppliers), "avg_quality_rating": round(float(avg_quality_rating), 2), "avg_delivery_rating": round(float(avg_delivery_rating), 2), "total_spend": float(total_spend) } async def approve_supplier( self, supplier_id: UUID, approved_by: UUID, approval_date: Optional[datetime] = None ) -> Optional[Supplier]: """Approve a pending supplier""" supplier = await self.get_by_id(supplier_id) if not supplier or supplier.status != SupplierStatus.pending_approval: return None supplier.status = SupplierStatus.active supplier.approved_by = approved_by supplier.approved_at = approval_date or datetime.utcnow() supplier.rejection_reason = None supplier.updated_at = datetime.utcnow() await self.db.commit() await self.db.refresh(supplier) return supplier async def reject_supplier( self, supplier_id: UUID, rejection_reason: str, approved_by: UUID ) -> Optional[Supplier]: """Reject a pending supplier""" supplier = await self.get_by_id(supplier_id) if not supplier or supplier.status != SupplierStatus.pending_approval: return None supplier.status = SupplierStatus.inactive supplier.rejection_reason = rejection_reason supplier.approved_by = approved_by supplier.approved_at = datetime.utcnow() supplier.updated_at = datetime.utcnow() await self.db.commit() await self.db.refresh(supplier) return supplier async def hard_delete_supplier(self, supplier_id: UUID) -> Dict[str, Any]: """ Hard delete supplier and all associated data Returns counts of deleted records """ from app.models.suppliers import ( SupplierPriceList, SupplierQualityReview, SupplierAlert, SupplierScorecard, PurchaseOrderStatus, PurchaseOrder ) from app.models.performance import SupplierPerformanceMetric from sqlalchemy import delete # Get supplier first supplier = await self.get_by_id(supplier_id) if not supplier: return None # Check for active purchase orders (block deletion if any exist) active_statuses = [ PurchaseOrderStatus.draft, PurchaseOrderStatus.pending_approval, PurchaseOrderStatus.approved, PurchaseOrderStatus.sent_to_supplier, PurchaseOrderStatus.confirmed ] stmt = select(PurchaseOrder).where( PurchaseOrder.supplier_id == supplier_id, PurchaseOrder.status.in_(active_statuses) ) result = await self.db.execute(stmt) active_pos = result.scalars().all() if active_pos: raise ValueError( f"Cannot delete supplier with {len(active_pos)} active purchase orders. " "Complete or cancel all purchase orders first." ) # Count related records before deletion stmt = select(SupplierPriceList).where(SupplierPriceList.supplier_id == supplier_id) result = await self.db.execute(stmt) price_lists_count = len(result.scalars().all()) stmt = select(SupplierQualityReview).where(SupplierQualityReview.supplier_id == supplier_id) result = await self.db.execute(stmt) quality_reviews_count = len(result.scalars().all()) stmt = select(SupplierPerformanceMetric).where(SupplierPerformanceMetric.supplier_id == supplier_id) result = await self.db.execute(stmt) metrics_count = len(result.scalars().all()) stmt = select(SupplierAlert).where(SupplierAlert.supplier_id == supplier_id) result = await self.db.execute(stmt) alerts_count = len(result.scalars().all()) stmt = select(SupplierScorecard).where(SupplierScorecard.supplier_id == supplier_id) result = await self.db.execute(stmt) scorecards_count = len(result.scalars().all()) # Delete related records (in reverse dependency order) stmt = delete(SupplierScorecard).where(SupplierScorecard.supplier_id == supplier_id) await self.db.execute(stmt) stmt = delete(SupplierAlert).where(SupplierAlert.supplier_id == supplier_id) await self.db.execute(stmt) stmt = delete(SupplierPerformanceMetric).where(SupplierPerformanceMetric.supplier_id == supplier_id) await self.db.execute(stmt) stmt = delete(SupplierQualityReview).where(SupplierQualityReview.supplier_id == supplier_id) await self.db.execute(stmt) stmt = delete(SupplierPriceList).where(SupplierPriceList.supplier_id == supplier_id) await self.db.execute(stmt) # Delete the supplier itself await self.delete(supplier_id) await self.db.commit() return { "supplier_name": supplier.name, "deleted_price_lists": price_lists_count, "deleted_quality_reviews": quality_reviews_count, "deleted_performance_metrics": metrics_count, "deleted_alerts": alerts_count, "deleted_scorecards": scorecards_count, "deletion_timestamp": datetime.utcnow() } async def get_supplier_price_lists( self, supplier_id: UUID, tenant_id: UUID, is_active: bool = True ) -> List[Any]: """Get all price list items for a supplier""" from app.models.suppliers import SupplierPriceList stmt = select(SupplierPriceList).filter( and_( SupplierPriceList.supplier_id == supplier_id, SupplierPriceList.tenant_id == tenant_id ) ) if is_active: stmt = stmt.filter(SupplierPriceList.is_active == True) result = await self.db.execute(stmt) return result.scalars().all() async def get_supplier_price_list( self, price_list_id: UUID, tenant_id: UUID ) -> Optional[Any]: """Get specific price list item""" from app.models.suppliers import SupplierPriceList stmt = select(SupplierPriceList).filter( and_( SupplierPriceList.id == price_list_id, SupplierPriceList.tenant_id == tenant_id ) ) result = await self.db.execute(stmt) return result.scalar_one_or_none() async def create_supplier_price_list( self, create_data: Dict[str, Any] ) -> Any: """Create a new price list item""" from app.models.suppliers import SupplierPriceList price_list = SupplierPriceList(**create_data) self.db.add(price_list) await self.db.commit() await self.db.refresh(price_list) return price_list async def update_supplier_price_list( self, price_list_id: UUID, update_data: Dict[str, Any] ) -> Any: """Update a price list item""" from app.models.suppliers import SupplierPriceList stmt = select(SupplierPriceList).filter(SupplierPriceList.id == price_list_id) result = await self.db.execute(stmt) price_list = result.scalar_one_or_none() if not price_list: raise ValueError("Price list item not found") # Update fields for key, value in update_data.items(): if hasattr(price_list, key): setattr(price_list, key, value) await self.db.commit() await self.db.refresh(price_list) return price_list async def delete_supplier_price_list( self, price_list_id: UUID ) -> bool: """Delete a price list item""" from app.models.suppliers import SupplierPriceList from sqlalchemy import delete stmt = delete(SupplierPriceList).filter(SupplierPriceList.id == price_list_id) result = await self.db.execute(stmt) await self.db.commit() return result.rowcount > 0