New enterprise feature

This commit is contained in:
Urtzi Alfaro
2025-11-30 09:12:40 +01:00
parent f9d0eec6ec
commit 972db02f6d
176 changed files with 19741 additions and 1361 deletions

View File

@@ -0,0 +1,207 @@
"""
Delivery Route Repository
"""
from typing import List, Dict, Any, Optional
from datetime import date, datetime
import uuid
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.future import select
from app.models.distribution import DeliveryRoute, DeliveryRouteStatus
from shared.database.base import Base
class DeliveryRouteRepository:
def __init__(self, db_session: AsyncSession):
self.db_session = db_session
async def create_route(self, route_data: Dict[str, Any]) -> Dict[str, Any]:
"""
Create a new delivery route
"""
# Define system user ID to use when user_id is not provided
SYSTEM_USER_ID = uuid.UUID("50000000-0000-0000-0000-000000000004")
route = DeliveryRoute(
id=uuid.uuid4(),
tenant_id=route_data['tenant_id'],
route_number=route_data['route_number'],
route_date=route_data['route_date'],
vehicle_id=route_data.get('vehicle_id'),
driver_id=route_data.get('driver_id'),
total_distance_km=route_data.get('total_distance_km'),
estimated_duration_minutes=route_data.get('estimated_duration_minutes'),
route_sequence=route_data.get('route_sequence'),
status=route_data.get('status', 'planned'),
created_by=route_data.get('created_by', SYSTEM_USER_ID),
updated_by=route_data.get('updated_by', SYSTEM_USER_ID)
)
self.db_session.add(route)
await self.db_session.commit()
await self.db_session.refresh(route)
# Convert SQLAlchemy object to dict for return
return {
'id': str(route.id),
'tenant_id': str(route.tenant_id),
'route_number': route.route_number,
'route_date': route.route_date,
'vehicle_id': route.vehicle_id,
'driver_id': route.driver_id,
'total_distance_km': route.total_distance_km,
'estimated_duration_minutes': route.estimated_duration_minutes,
'route_sequence': route.route_sequence,
'status': route.status.value if hasattr(route.status, 'value') else route.status,
'created_at': route.created_at,
'updated_at': route.updated_at
}
async def get_routes_by_date(self, tenant_id: str, target_date: date) -> List[Dict[str, Any]]:
"""
Get all delivery routes for a specific date and tenant
"""
stmt = select(DeliveryRoute).where(
(DeliveryRoute.tenant_id == tenant_id) &
(DeliveryRoute.route_date >= datetime.combine(target_date, datetime.min.time())) &
(DeliveryRoute.route_date < datetime.combine(target_date, datetime.max.time().replace(hour=23, minute=59, second=59)))
)
result = await self.db_session.execute(stmt)
routes = result.scalars().all()
return [
{
'id': str(route.id),
'tenant_id': str(route.tenant_id),
'route_number': route.route_number,
'route_date': route.route_date,
'vehicle_id': route.vehicle_id,
'driver_id': route.driver_id,
'total_distance_km': route.total_distance_km,
'estimated_duration_minutes': route.estimated_duration_minutes,
'route_sequence': route.route_sequence,
'status': route.status.value if hasattr(route.status, 'value') else route.status,
'created_at': route.created_at,
'updated_at': route.updated_at
}
for route in routes
]
async def get_routes_by_date_range(self, tenant_id: str, start_date: date, end_date: date) -> List[Dict[str, Any]]:
"""
Get all delivery routes for a specific date range and tenant
"""
stmt = select(DeliveryRoute).where(
(DeliveryRoute.tenant_id == tenant_id) &
(DeliveryRoute.route_date >= datetime.combine(start_date, datetime.min.time())) &
(DeliveryRoute.route_date <= datetime.combine(end_date, datetime.max.time().replace(hour=23, minute=59, second=59)))
)
result = await self.db_session.execute(stmt)
routes = result.scalars().all()
return [
{
'id': str(route.id),
'tenant_id': str(route.tenant_id),
'route_number': route.route_number,
'route_date': route.route_date,
'vehicle_id': route.vehicle_id,
'driver_id': route.driver_id,
'total_distance_km': route.total_distance_km,
'estimated_duration_minutes': route.estimated_duration_minutes,
'route_sequence': route.route_sequence,
'status': route.status.value if hasattr(route.status, 'value') else route.status,
'created_at': route.created_at,
'updated_at': route.updated_at
}
for route in routes
]
async def get_route_by_id(self, route_id: str) -> Optional[Dict[str, Any]]:
"""
Get a specific delivery route by ID
"""
stmt = select(DeliveryRoute).where(DeliveryRoute.id == route_id)
result = await self.db_session.execute(stmt)
route = result.scalar_one_or_none()
if route:
return {
'id': str(route.id),
'tenant_id': str(route.tenant_id),
'route_number': route.route_number,
'route_date': route.route_date,
'vehicle_id': route.vehicle_id,
'driver_id': route.driver_id,
'total_distance_km': route.total_distance_km,
'estimated_duration_minutes': route.estimated_duration_minutes,
'route_sequence': route.route_sequence,
'status': route.status.value if hasattr(route.status, 'value') else route.status,
'created_at': route.created_at,
'updated_at': route.updated_at
}
return None
async def update_route_status(self, route_id: str, status: str, user_id: str) -> Optional[Dict[str, Any]]:
"""
Update route status
"""
stmt = select(DeliveryRoute).where(DeliveryRoute.id == route_id)
result = await self.db_session.execute(stmt)
route = result.scalar_one_or_none()
if not route:
return None
# Handle system user ID if passed as string
if user_id == 'system':
SYSTEM_USER_ID = uuid.UUID("50000000-0000-0000-0000-000000000004")
route.updated_by = SYSTEM_USER_ID
else:
route.updated_by = user_id
route.status = status
await self.db_session.commit()
await self.db_session.refresh(route)
return {
'id': str(route.id),
'tenant_id': str(route.tenant_id),
'route_number': route.route_number,
'route_date': route.route_date,
'vehicle_id': route.vehicle_id,
'driver_id': route.driver_id,
'total_distance_km': route.total_distance_km,
'estimated_duration_minutes': route.estimated_duration_minutes,
'route_sequence': route.route_sequence,
'status': route.status.value if hasattr(route.status, 'value') else route.status,
'created_at': route.created_at,
'updated_at': route.updated_at
}
async def delete_demo_routes_for_tenant(self, tenant_id: str) -> int:
"""
Delete all demo routes for a tenant
Used for demo session cleanup
Args:
tenant_id: The tenant ID to delete routes for
Returns:
Number of routes deleted
"""
from sqlalchemy import delete
# Delete routes with DEMO- prefix in route_number
stmt = delete(DeliveryRoute).where(
(DeliveryRoute.tenant_id == uuid.UUID(tenant_id)) &
(DeliveryRoute.route_number.like('DEMO-%'))
)
result = await self.db_session.execute(stmt)
await self.db_session.commit()
deleted_count = result.rowcount
return deleted_count

View File

@@ -0,0 +1,74 @@
from typing import List, Optional, Dict, Any
from uuid import UUID
from sqlalchemy import select, update, delete
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.exc import IntegrityError
import structlog
from app.models.distribution import DeliverySchedule
logger = structlog.get_logger()
class DeliveryScheduleRepository:
def __init__(self, session: AsyncSession):
self.session = session
async def create_schedule(self, schedule_data: Dict[str, Any]) -> DeliverySchedule:
"""Create a new delivery schedule"""
try:
schedule = DeliverySchedule(**schedule_data)
self.session.add(schedule)
await self.session.commit()
await self.session.refresh(schedule)
return schedule
except IntegrityError as e:
await self.session.rollback()
logger.error("Error creating delivery schedule", error=str(e))
raise ValueError(f"Failed to create delivery schedule: {e}")
except Exception as e:
await self.session.rollback()
logger.error("Unexpected error creating delivery schedule", error=str(e))
raise
async def get_schedule_by_id(self, schedule_id: UUID) -> Optional[DeliverySchedule]:
"""Get a delivery schedule by ID"""
result = await self.session.execute(
select(DeliverySchedule).where(DeliverySchedule.id == schedule_id)
)
return result.scalar_one_or_none()
async def get_schedules_by_tenant(self, tenant_id: UUID) -> List[DeliverySchedule]:
"""Get all delivery schedules for a tenant"""
result = await self.session.execute(
select(DeliverySchedule).where(DeliverySchedule.tenant_id == tenant_id)
)
return result.scalars().all()
async def update_schedule(self, schedule_id: UUID, update_data: Dict[str, Any]) -> Optional[DeliverySchedule]:
"""Update a delivery schedule"""
try:
stmt = (
update(DeliverySchedule)
.where(DeliverySchedule.id == schedule_id)
.values(**update_data)
.returning(DeliverySchedule)
)
result = await self.session.execute(stmt)
await self.session.commit()
return result.scalar_one_or_none()
except Exception as e:
await self.session.rollback()
logger.error("Error updating delivery schedule", error=str(e), schedule_id=schedule_id)
raise
async def delete_schedule(self, schedule_id: UUID) -> bool:
"""Delete a delivery schedule"""
try:
stmt = delete(DeliverySchedule).where(DeliverySchedule.id == schedule_id)
result = await self.session.execute(stmt)
await self.session.commit()
return result.rowcount > 0
except Exception as e:
await self.session.rollback()
logger.error("Error deleting delivery schedule", error=str(e), schedule_id=schedule_id)
raise

View File

@@ -0,0 +1,309 @@
"""
Shipment Repository
"""
from typing import List, Dict, Any, Optional
from datetime import date, datetime
import uuid
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.future import select
from app.models.distribution import Shipment, ShipmentStatus
from shared.database.base import Base
class ShipmentRepository:
def __init__(self, db_session: AsyncSession):
self.db_session = db_session
async def create_shipment(self, shipment_data: Dict[str, Any]) -> Dict[str, Any]:
"""
Create a new shipment
"""
# Define system user ID to use when user_id is not provided
SYSTEM_USER_ID = uuid.UUID("50000000-0000-0000-0000-000000000004")
shipment = Shipment(
id=uuid.uuid4(),
tenant_id=shipment_data['tenant_id'],
parent_tenant_id=shipment_data['parent_tenant_id'],
child_tenant_id=shipment_data['child_tenant_id'],
purchase_order_id=shipment_data.get('purchase_order_id'),
delivery_route_id=shipment_data.get('delivery_route_id'),
shipment_number=shipment_data['shipment_number'],
shipment_date=shipment_data['shipment_date'],
status=shipment_data.get('status', 'pending'),
total_weight_kg=shipment_data.get('total_weight_kg'),
total_volume_m3=shipment_data.get('total_volume_m3'),
created_by=shipment_data.get('created_by', SYSTEM_USER_ID),
updated_by=shipment_data.get('updated_by', SYSTEM_USER_ID)
)
self.db_session.add(shipment)
await self.db_session.commit()
await self.db_session.refresh(shipment)
# Convert SQLAlchemy object to dict for return
return {
'id': str(shipment.id),
'tenant_id': str(shipment.tenant_id),
'parent_tenant_id': str(shipment.parent_tenant_id),
'child_tenant_id': str(shipment.child_tenant_id),
'purchase_order_id': str(shipment.purchase_order_id) if shipment.purchase_order_id else None,
'delivery_route_id': str(shipment.delivery_route_id) if shipment.delivery_route_id else None,
'shipment_number': shipment.shipment_number,
'shipment_date': shipment.shipment_date,
'current_location_lat': shipment.current_location_lat,
'current_location_lng': shipment.current_location_lng,
'last_tracked_at': shipment.last_tracked_at,
'status': shipment.status.value if hasattr(shipment.status, 'value') else shipment.status,
'actual_delivery_time': shipment.actual_delivery_time,
'signature': shipment.signature,
'photo_url': shipment.photo_url,
'received_by_name': shipment.received_by_name,
'delivery_notes': shipment.delivery_notes,
'total_weight_kg': shipment.total_weight_kg,
'total_volume_m3': shipment.total_volume_m3,
'created_at': shipment.created_at,
'updated_at': shipment.updated_at
}
async def get_shipments_by_date(self, tenant_id: str, target_date: date) -> List[Dict[str, Any]]:
"""
Get all shipments for a specific date and tenant
"""
stmt = select(Shipment).where(
(Shipment.tenant_id == tenant_id) &
(Shipment.shipment_date >= datetime.combine(target_date, datetime.min.time())) &
(Shipment.shipment_date < datetime.combine(target_date, datetime.max.time().replace(hour=23, minute=59, second=59)))
)
result = await self.db_session.execute(stmt)
shipments = result.scalars().all()
return [
{
'id': str(shipment.id),
'tenant_id': str(shipment.tenant_id),
'parent_tenant_id': str(shipment.parent_tenant_id),
'child_tenant_id': str(shipment.child_tenant_id),
'purchase_order_id': str(shipment.purchase_order_id) if shipment.purchase_order_id else None,
'delivery_route_id': str(shipment.delivery_route_id) if shipment.delivery_route_id else None,
'shipment_number': shipment.shipment_number,
'shipment_date': shipment.shipment_date,
'current_location_lat': shipment.current_location_lat,
'current_location_lng': shipment.current_location_lng,
'last_tracked_at': shipment.last_tracked_at,
'status': shipment.status.value if hasattr(shipment.status, 'value') else shipment.status,
'actual_delivery_time': shipment.actual_delivery_time,
'signature': shipment.signature,
'photo_url': shipment.photo_url,
'received_by_name': shipment.received_by_name,
'delivery_notes': shipment.delivery_notes,
'total_weight_kg': shipment.total_weight_kg,
'total_volume_m3': shipment.total_volume_m3,
'created_at': shipment.created_at,
'updated_at': shipment.updated_at
}
for shipment in shipments
]
async def get_shipments_by_date_range(self, tenant_id: str, start_date: date, end_date: date) -> List[Dict[str, Any]]:
"""
Get all shipments for a specific date range and tenant
"""
stmt = select(Shipment).where(
(Shipment.tenant_id == tenant_id) &
(Shipment.shipment_date >= datetime.combine(start_date, datetime.min.time())) &
(Shipment.shipment_date <= datetime.combine(end_date, datetime.max.time().replace(hour=23, minute=59, second=59)))
)
result = await self.db_session.execute(stmt)
shipments = result.scalars().all()
return [
{
'id': str(shipment.id),
'tenant_id': str(shipment.tenant_id),
'parent_tenant_id': str(shipment.parent_tenant_id),
'child_tenant_id': str(shipment.child_tenant_id),
'purchase_order_id': str(shipment.purchase_order_id) if shipment.purchase_order_id else None,
'delivery_route_id': str(shipment.delivery_route_id) if shipment.delivery_route_id else None,
'shipment_number': shipment.shipment_number,
'shipment_date': shipment.shipment_date,
'current_location_lat': shipment.current_location_lat,
'current_location_lng': shipment.current_location_lng,
'last_tracked_at': shipment.last_tracked_at,
'status': shipment.status.value if hasattr(shipment.status, 'value') else shipment.status,
'actual_delivery_time': shipment.actual_delivery_time,
'signature': shipment.signature,
'photo_url': shipment.photo_url,
'received_by_name': shipment.received_by_name,
'delivery_notes': shipment.delivery_notes,
'total_weight_kg': shipment.total_weight_kg,
'total_volume_m3': shipment.total_volume_m3,
'created_at': shipment.created_at,
'updated_at': shipment.updated_at
}
for shipment in shipments
]
async def get_shipment_by_id(self, shipment_id: str) -> Optional[Dict[str, Any]]:
"""
Get a specific shipment by ID
"""
stmt = select(Shipment).where(Shipment.id == shipment_id)
result = await self.db_session.execute(stmt)
shipment = result.scalar_one_or_none()
if shipment:
return {
'id': str(shipment.id),
'tenant_id': str(shipment.tenant_id),
'parent_tenant_id': str(shipment.parent_tenant_id),
'child_tenant_id': str(shipment.child_tenant_id),
'purchase_order_id': str(shipment.purchase_order_id) if shipment.purchase_order_id else None,
'delivery_route_id': str(shipment.delivery_route_id) if shipment.delivery_route_id else None,
'shipment_number': shipment.shipment_number,
'shipment_date': shipment.shipment_date,
'current_location_lat': shipment.current_location_lat,
'current_location_lng': shipment.current_location_lng,
'last_tracked_at': shipment.last_tracked_at,
'status': shipment.status.value if hasattr(shipment.status, 'value') else shipment.status,
'actual_delivery_time': shipment.actual_delivery_time,
'signature': shipment.signature,
'photo_url': shipment.photo_url,
'received_by_name': shipment.received_by_name,
'delivery_notes': shipment.delivery_notes,
'total_weight_kg': shipment.total_weight_kg,
'total_volume_m3': shipment.total_volume_m3,
'created_at': shipment.created_at,
'updated_at': shipment.updated_at
}
return None
async def update_shipment_status(self, shipment_id: str, status: str, user_id: str, metadata: Optional[Dict[str, Any]] = None) -> Optional[Dict[str, Any]]:
"""
Update shipment status
"""
stmt = select(Shipment).where(Shipment.id == shipment_id)
result = await self.db_session.execute(stmt)
shipment = result.scalar_one_or_none()
if not shipment:
return None
# Handle system user ID if passed as string
if user_id == 'system':
SYSTEM_USER_ID = uuid.UUID("50000000-0000-0000-0000-000000000004")
shipment.updated_by = SYSTEM_USER_ID
else:
shipment.updated_by = user_id
shipment.status = status
# Update tracking information if provided in metadata
if metadata:
if 'current_location_lat' in metadata:
shipment.current_location_lat = metadata['current_location_lat']
if 'current_location_lng' in metadata:
shipment.current_location_lng = metadata['current_location_lng']
if 'last_tracked_at' in metadata:
from datetime import datetime
shipment.last_tracked_at = datetime.fromisoformat(metadata['last_tracked_at']) if isinstance(metadata['last_tracked_at'], str) else metadata['last_tracked_at']
if 'signature' in metadata:
shipment.signature = metadata['signature']
if 'photo_url' in metadata:
shipment.photo_url = metadata['photo_url']
if 'received_by_name' in metadata:
shipment.received_by_name = metadata['received_by_name']
if 'delivery_notes' in metadata:
shipment.delivery_notes = metadata['delivery_notes']
if 'actual_delivery_time' in metadata:
from datetime import datetime
shipment.actual_delivery_time = datetime.fromisoformat(metadata['actual_delivery_time']) if isinstance(metadata['actual_delivery_time'], str) else metadata['actual_delivery_time']
await self.db_session.commit()
await self.db_session.refresh(shipment)
return {
'id': str(shipment.id),
'tenant_id': str(shipment.tenant_id),
'parent_tenant_id': str(shipment.parent_tenant_id),
'child_tenant_id': str(shipment.child_tenant_id),
'purchase_order_id': str(shipment.purchase_order_id) if shipment.purchase_order_id else None,
'delivery_route_id': str(shipment.delivery_route_id) if shipment.delivery_route_id else None,
'shipment_number': shipment.shipment_number,
'shipment_date': shipment.shipment_date,
'current_location_lat': shipment.current_location_lat,
'current_location_lng': shipment.current_location_lng,
'last_tracked_at': shipment.last_tracked_at,
'status': shipment.status.value if hasattr(shipment.status, 'value') else shipment.status,
'actual_delivery_time': shipment.actual_delivery_time,
'signature': shipment.signature,
'photo_url': shipment.photo_url,
'received_by_name': shipment.received_by_name,
'delivery_notes': shipment.delivery_notes,
'total_weight_kg': shipment.total_weight_kg,
'total_volume_m3': shipment.total_volume_m3,
'created_at': shipment.created_at,
'updated_at': shipment.updated_at
}
async def assign_shipments_to_route(self, route_id: str, shipment_ids: List[str], user_id: str) -> Dict[str, Any]:
"""
Assign multiple shipments to a specific route
"""
stmt = select(Shipment).where(Shipment.id.in_(shipment_ids))
result = await self.db_session.execute(stmt)
shipments = result.scalars().all()
# Handle system user ID if passed as string
actual_user_id = user_id
if user_id == 'system':
actual_user_id = uuid.UUID("50000000-0000-0000-0000-000000000004")
updated_shipments = []
for shipment in shipments:
shipment.delivery_route_id = route_id
shipment.updated_by = actual_user_id
await self.db_session.refresh(shipment)
updated_shipments.append({
'id': str(shipment.id),
'shipment_number': shipment.shipment_number,
'status': shipment.status.value if hasattr(shipment.status, 'value') else shipment.status,
'delivery_route_id': str(shipment.delivery_route_id)
})
await self.db_session.commit()
return {
'route_id': route_id,
'updated_shipments': updated_shipments,
'count': len(updated_shipments)
}
async def delete_demo_shipments_for_tenant(self, tenant_id: str) -> int:
"""
Delete all demo shipments for a tenant
Used for demo session cleanup
Args:
tenant_id: The tenant ID to delete shipments for
Returns:
Number of shipments deleted
"""
from sqlalchemy import delete
# Delete shipments with DEMOSHP- prefix in shipment_number
stmt = delete(Shipment).where(
(Shipment.tenant_id == uuid.UUID(tenant_id)) &
(Shipment.shipment_number.like('DEMOSHP-%'))
)
result = await self.db_session.execute(stmt)
await self.db_session.commit()
deleted_count = result.rowcount
return deleted_count