# services/suppliers/app/services/delivery_service.py """ Delivery service for business logic operations """ import structlog from typing import List, Optional, Dict, Any from uuid import UUID from datetime import datetime from sqlalchemy.orm import Session from app.repositories.delivery_repository import DeliveryRepository from app.repositories.purchase_order_repository import PurchaseOrderRepository from app.repositories.supplier_repository import SupplierRepository from app.models.suppliers import ( Delivery, DeliveryItem, DeliveryStatus, PurchaseOrder, PurchaseOrderStatus, SupplierStatus ) from app.schemas.suppliers import ( DeliveryCreate, DeliveryUpdate, DeliverySearchParams ) from app.core.config import settings logger = structlog.get_logger() class DeliveryService: """Service for delivery management operations""" def __init__(self, db: Session): self.db = db self.repository = DeliveryRepository(db) self.po_repository = PurchaseOrderRepository(db) self.supplier_repository = SupplierRepository(db) async def create_delivery( self, tenant_id: UUID, delivery_data: DeliveryCreate, created_by: UUID ) -> Delivery: """Create a new delivery""" logger.info( "Creating delivery", tenant_id=str(tenant_id), po_id=str(delivery_data.purchase_order_id) ) # Validate purchase order exists and belongs to tenant po = self.po_repository.get_by_id(delivery_data.purchase_order_id) if not po: raise ValueError("Purchase order not found") if po.tenant_id != tenant_id: raise ValueError("Purchase order does not belong to this tenant") if po.status not in [ PurchaseOrderStatus.CONFIRMED, PurchaseOrderStatus.PARTIALLY_RECEIVED ]: raise ValueError("Purchase order must be confirmed before creating deliveries") # Validate supplier supplier = self.supplier_repository.get_by_id(delivery_data.supplier_id) if not supplier: raise ValueError("Supplier not found") if supplier.id != po.supplier_id: raise ValueError("Supplier does not match purchase order supplier") # Generate delivery number delivery_number = self.repository.generate_delivery_number(tenant_id) # Create delivery delivery_create_data = delivery_data.model_dump(exclude={'items'}) delivery_create_data.update({ 'tenant_id': tenant_id, 'delivery_number': delivery_number, 'status': DeliveryStatus.SCHEDULED, 'created_by': created_by }) # Set default scheduled date if not provided if not delivery_create_data.get('scheduled_date'): delivery_create_data['scheduled_date'] = datetime.utcnow() delivery = self.repository.create(delivery_create_data) # Create delivery items from app.repositories.purchase_order_item_repository import PurchaseOrderItemRepository item_repo = PurchaseOrderItemRepository(self.db) for item_data in delivery_data.items: # Validate purchase order item po_item = item_repo.get_by_id(item_data.purchase_order_item_id) if not po_item or po_item.purchase_order_id != po.id: raise ValueError("Invalid purchase order item") # Create delivery item from app.models.suppliers import DeliveryItem item_create_data = item_data.model_dump() item_create_data.update({ 'tenant_id': tenant_id, 'delivery_id': delivery.id }) delivery_item = DeliveryItem(**item_create_data) self.db.add(delivery_item) self.db.commit() logger.info( "Delivery created successfully", tenant_id=str(tenant_id), delivery_id=str(delivery.id), delivery_number=delivery_number ) return delivery async def get_delivery(self, delivery_id: UUID) -> Optional[Delivery]: """Get delivery by ID with items""" return self.repository.get_with_items(delivery_id) async def update_delivery( self, delivery_id: UUID, delivery_data: DeliveryUpdate, updated_by: UUID ) -> Optional[Delivery]: """Update delivery information""" logger.info("Updating delivery", delivery_id=str(delivery_id)) delivery = self.repository.get_by_id(delivery_id) if not delivery: return None # Check if delivery can be modified if delivery.status in [DeliveryStatus.DELIVERED, DeliveryStatus.FAILED_DELIVERY]: raise ValueError("Cannot modify completed deliveries") # Prepare update data update_data = delivery_data.model_dump(exclude_unset=True) update_data['created_by'] = updated_by # Track who updated update_data['updated_at'] = datetime.utcnow() delivery = self.repository.update(delivery_id, update_data) logger.info("Delivery updated successfully", delivery_id=str(delivery_id)) return delivery async def update_delivery_status( self, delivery_id: UUID, status: DeliveryStatus, updated_by: UUID, notes: Optional[str] = None, update_timestamps: bool = True ) -> Optional[Delivery]: """Update delivery status""" logger.info("Updating delivery status", delivery_id=str(delivery_id), status=status.value) return self.repository.update_delivery_status( delivery_id=delivery_id, status=status, updated_by=updated_by, notes=notes, update_timestamps=update_timestamps ) async def mark_as_received( self, delivery_id: UUID, received_by: UUID, inspection_passed: bool = True, inspection_notes: Optional[str] = None, quality_issues: Optional[Dict[str, Any]] = None, notes: Optional[str] = None ) -> Optional[Delivery]: """Mark delivery as received with inspection details""" logger.info("Marking delivery as received", delivery_id=str(delivery_id)) delivery = self.repository.mark_as_received( delivery_id=delivery_id, received_by=received_by, inspection_passed=inspection_passed, inspection_notes=inspection_notes, quality_issues=quality_issues ) if not delivery: return None # Add custom notes if provided if notes: existing_notes = delivery.notes or "" timestamp = datetime.utcnow().strftime("%Y-%m-%d %H:%M") delivery.notes = f"{existing_notes}\n[{timestamp}] Receipt notes: {notes}".strip() self.repository.update(delivery_id, {'notes': delivery.notes}) # Update purchase order item received quantities await self._update_purchase_order_received_quantities(delivery) # Check if purchase order is fully received await self._check_purchase_order_completion(delivery.purchase_order_id) logger.info("Delivery marked as received", delivery_id=str(delivery_id)) return delivery async def search_deliveries( self, tenant_id: UUID, search_params: DeliverySearchParams ) -> List[Delivery]: """Search deliveries with filters""" return self.repository.search_deliveries( tenant_id=tenant_id, supplier_id=search_params.supplier_id, status=search_params.status, date_from=search_params.date_from, date_to=search_params.date_to, search_term=search_params.search_term, limit=search_params.limit, offset=search_params.offset ) async def get_deliveries_by_purchase_order(self, po_id: UUID) -> List[Delivery]: """Get all deliveries for a purchase order""" return self.repository.get_by_purchase_order(po_id) async def get_todays_deliveries(self, tenant_id: UUID) -> List[Delivery]: """Get deliveries scheduled for today""" return self.repository.get_todays_deliveries(tenant_id) async def get_overdue_deliveries(self, tenant_id: UUID) -> List[Delivery]: """Get overdue deliveries""" return self.repository.get_overdue_deliveries(tenant_id) async def get_scheduled_deliveries( self, tenant_id: UUID, date_from: Optional[datetime] = None, date_to: Optional[datetime] = None ) -> List[Delivery]: """Get scheduled deliveries for date range""" return self.repository.get_scheduled_deliveries(tenant_id, date_from, date_to) async def get_delivery_performance_stats( self, tenant_id: UUID, days_back: int = 30, supplier_id: Optional[UUID] = None ) -> Dict[str, Any]: """Get delivery performance statistics""" return self.repository.get_delivery_performance_stats( tenant_id, days_back, supplier_id ) async def get_upcoming_deliveries_summary(self, tenant_id: UUID) -> Dict[str, Any]: """Get summary of upcoming deliveries""" return self.repository.get_upcoming_deliveries_summary(tenant_id) async def _update_purchase_order_received_quantities(self, delivery: Delivery): """Update purchase order item received quantities based on delivery""" from app.repositories.purchase_order_item_repository import PurchaseOrderItemRepository item_repo = PurchaseOrderItemRepository(self.db) # Get delivery items with accepted quantities delivery_with_items = self.repository.get_with_items(delivery.id) if not delivery_with_items or not delivery_with_items.items: return for delivery_item in delivery_with_items.items: # Update purchase order item received quantity item_repo.add_received_quantity( delivery_item.purchase_order_item_id, delivery_item.accepted_quantity ) async def _check_purchase_order_completion(self, po_id: UUID): """Check if purchase order is fully received and update status""" from app.repositories.purchase_order_item_repository import PurchaseOrderItemRepository item_repo = PurchaseOrderItemRepository(self.db) po_items = item_repo.get_by_purchase_order(po_id) if not po_items: return # Check if all items are fully received fully_received = all(item.remaining_quantity == 0 for item in po_items) partially_received = any(item.received_quantity > 0 for item in po_items) if fully_received: # Mark purchase order as completed self.po_repository.update_order_status( po_id, PurchaseOrderStatus.COMPLETED, po_items[0].tenant_id, # Use tenant_id as updated_by placeholder "All items received" ) elif partially_received: # Mark as partially received if not already po = self.po_repository.get_by_id(po_id) if po and po.status == PurchaseOrderStatus.CONFIRMED: self.po_repository.update_order_status( po_id, PurchaseOrderStatus.PARTIALLY_RECEIVED, po.tenant_id, # Use tenant_id as updated_by placeholder "Partial delivery received" ) async def generate_delivery_tracking_info(self, delivery_id: UUID) -> Dict[str, Any]: """Generate delivery tracking information""" delivery = self.repository.get_with_items(delivery_id) if not delivery: return {} # Calculate delivery metrics total_items = len(delivery.items) if delivery.items else 0 delivered_items = sum( 1 for item in (delivery.items or []) if item.delivered_quantity > 0 ) accepted_items = sum( 1 for item in (delivery.items or []) if item.accepted_quantity > 0 ) rejected_items = sum( 1 for item in (delivery.items or []) if item.rejected_quantity > 0 ) # Calculate timing metrics on_time = False delay_hours = 0 if delivery.scheduled_date and delivery.actual_arrival: delay_seconds = (delivery.actual_arrival - delivery.scheduled_date).total_seconds() delay_hours = delay_seconds / 3600 on_time = delay_hours <= 0 return { "delivery_id": str(delivery.id), "delivery_number": delivery.delivery_number, "status": delivery.status.value, "total_items": total_items, "delivered_items": delivered_items, "accepted_items": accepted_items, "rejected_items": rejected_items, "inspection_passed": delivery.inspection_passed, "on_time": on_time, "delay_hours": round(delay_hours, 1) if delay_hours > 0 else 0, "quality_issues": delivery.quality_issues or {}, "scheduled_date": delivery.scheduled_date.isoformat() if delivery.scheduled_date else None, "actual_arrival": delivery.actual_arrival.isoformat() if delivery.actual_arrival else None, "completed_at": delivery.completed_at.isoformat() if delivery.completed_at else None }