Initial commit - production deployment
This commit is contained in:
322
services/orders/app/api/customers.py
Normal file
322
services/orders/app/api/customers.py
Normal file
@@ -0,0 +1,322 @@
|
||||
# ================================================================
|
||||
# services/orders/app/api/customers.py
|
||||
# ================================================================
|
||||
"""
|
||||
Customers API endpoints - ATOMIC CRUD operations
|
||||
"""
|
||||
|
||||
from typing import List
|
||||
from uuid import UUID
|
||||
from fastapi import APIRouter, Depends, HTTPException, Path, Query, status
|
||||
import structlog
|
||||
|
||||
from shared.auth.decorators import get_current_user_dep
|
||||
from shared.auth.access_control import require_user_role
|
||||
from shared.routing import RouteBuilder
|
||||
from shared.security import create_audit_logger, AuditSeverity, AuditAction
|
||||
from app.core.database import get_db
|
||||
from app.services.orders_service import OrdersService
|
||||
from app.models import AuditLog
|
||||
from app.schemas.order_schemas import (
|
||||
CustomerCreate,
|
||||
CustomerUpdate,
|
||||
CustomerResponse
|
||||
)
|
||||
|
||||
logger = structlog.get_logger()
|
||||
audit_logger = create_audit_logger("orders-service", AuditLog)
|
||||
|
||||
# Create route builder for consistent URL structure
|
||||
route_builder = RouteBuilder('orders')
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# ===== Dependency Injection =====
|
||||
|
||||
async def get_orders_service(db = Depends(get_db)) -> OrdersService:
|
||||
"""Get orders service with dependencies"""
|
||||
from app.repositories.order_repository import (
|
||||
OrderRepository,
|
||||
CustomerRepository,
|
||||
OrderItemRepository,
|
||||
OrderStatusHistoryRepository
|
||||
)
|
||||
from shared.clients import (
|
||||
get_inventory_client,
|
||||
get_production_client,
|
||||
get_sales_client
|
||||
)
|
||||
|
||||
return OrdersService(
|
||||
order_repo=OrderRepository(),
|
||||
customer_repo=CustomerRepository(),
|
||||
order_item_repo=OrderItemRepository(),
|
||||
status_history_repo=OrderStatusHistoryRepository(),
|
||||
inventory_client=get_inventory_client(),
|
||||
production_client=get_production_client(),
|
||||
sales_client=get_sales_client()
|
||||
)
|
||||
|
||||
|
||||
# ===== Customer CRUD Endpoints =====
|
||||
|
||||
@router.post(
|
||||
route_builder.build_base_route("customers"),
|
||||
response_model=CustomerResponse,
|
||||
status_code=status.HTTP_201_CREATED
|
||||
)
|
||||
@require_user_role(['admin', 'owner', 'member'])
|
||||
async def create_customer(
|
||||
customer_data: CustomerCreate,
|
||||
tenant_id: UUID = Path(...),
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
orders_service: OrdersService = Depends(get_orders_service),
|
||||
db = Depends(get_db)
|
||||
):
|
||||
"""Create a new customer"""
|
||||
try:
|
||||
# Check if customer code already exists
|
||||
existing_customer = await orders_service.customer_repo.get_by_customer_code(
|
||||
db, customer_data.customer_code, tenant_id
|
||||
)
|
||||
if existing_customer:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Customer code already exists"
|
||||
)
|
||||
|
||||
# Extract user ID safely
|
||||
user_id = current_user.get("user_id")
|
||||
if not user_id:
|
||||
logger.error("User ID not found in current_user context", current_user_keys=list(current_user.keys()))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="User authentication error"
|
||||
)
|
||||
|
||||
customer = await orders_service.customer_repo.create(
|
||||
db,
|
||||
obj_in=customer_data,
|
||||
created_by=UUID(user_id),
|
||||
tenant_id=tenant_id
|
||||
)
|
||||
|
||||
# Commit the transaction to persist changes
|
||||
await db.commit()
|
||||
|
||||
logger.info("Customer created successfully",
|
||||
customer_id=str(customer.id),
|
||||
customer_code=customer.customer_code)
|
||||
|
||||
return CustomerResponse.from_orm(customer)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Error creating customer", error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to create customer"
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
route_builder.build_base_route("customers"),
|
||||
response_model=List[CustomerResponse]
|
||||
)
|
||||
async def get_customers(
|
||||
tenant_id: UUID = Path(...),
|
||||
active_only: bool = Query(True, description="Filter for active customers only"),
|
||||
skip: int = Query(0, ge=0, description="Number of customers to skip"),
|
||||
limit: int = Query(100, ge=1, le=1000, description="Number of customers to return"),
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
orders_service: OrdersService = Depends(get_orders_service),
|
||||
db = Depends(get_db)
|
||||
):
|
||||
"""Get customers with filtering and pagination"""
|
||||
try:
|
||||
if active_only:
|
||||
customers = await orders_service.customer_repo.get_active_customers(
|
||||
db, tenant_id, skip, limit
|
||||
)
|
||||
else:
|
||||
customers = await orders_service.customer_repo.get_multi(
|
||||
db, tenant_id, skip, limit, order_by="name"
|
||||
)
|
||||
|
||||
return [CustomerResponse.from_orm(customer) for customer in customers]
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Error getting customers", error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to retrieve customers"
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
route_builder.build_resource_detail_route("customers", "customer_id"),
|
||||
response_model=CustomerResponse
|
||||
)
|
||||
async def get_customer(
|
||||
tenant_id: UUID = Path(...),
|
||||
customer_id: UUID = Path(...),
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
orders_service: OrdersService = Depends(get_orders_service),
|
||||
db = Depends(get_db)
|
||||
):
|
||||
"""Get customer details by ID"""
|
||||
try:
|
||||
customer = await orders_service.customer_repo.get(db, customer_id, tenant_id)
|
||||
if not customer:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Customer not found"
|
||||
)
|
||||
|
||||
return CustomerResponse.from_orm(customer)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Error getting customer",
|
||||
customer_id=str(customer_id),
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to retrieve customer"
|
||||
)
|
||||
|
||||
|
||||
@router.put(
|
||||
route_builder.build_resource_detail_route("customers", "customer_id"),
|
||||
response_model=CustomerResponse
|
||||
)
|
||||
@require_user_role(['admin', 'owner', 'member'])
|
||||
async def update_customer(
|
||||
customer_data: CustomerUpdate,
|
||||
tenant_id: UUID = Path(...),
|
||||
customer_id: UUID = Path(...),
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
orders_service: OrdersService = Depends(get_orders_service),
|
||||
db = Depends(get_db)
|
||||
):
|
||||
"""Update customer information"""
|
||||
try:
|
||||
# Get existing customer
|
||||
customer = await orders_service.customer_repo.get(db, customer_id, tenant_id)
|
||||
if not customer:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Customer not found"
|
||||
)
|
||||
|
||||
# Update customer
|
||||
# Extract user ID safely for update
|
||||
user_id = current_user.get("user_id")
|
||||
if not user_id:
|
||||
logger.error("User ID not found in current_user context for update", current_user_keys=list(current_user.keys()))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="User authentication error"
|
||||
)
|
||||
|
||||
updated_customer = await orders_service.customer_repo.update(
|
||||
db,
|
||||
db_obj=customer,
|
||||
obj_in=customer_data.dict(exclude_unset=True),
|
||||
updated_by=UUID(user_id)
|
||||
)
|
||||
|
||||
# Commit the transaction to persist changes
|
||||
await db.commit()
|
||||
|
||||
logger.info("Customer updated successfully",
|
||||
customer_id=str(customer_id))
|
||||
|
||||
return CustomerResponse.from_orm(updated_customer)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Error updating customer",
|
||||
customer_id=str(customer_id),
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to update customer"
|
||||
)
|
||||
|
||||
|
||||
@router.delete(
|
||||
route_builder.build_resource_detail_route("customers", "customer_id"),
|
||||
status_code=status.HTTP_204_NO_CONTENT
|
||||
)
|
||||
@require_user_role(['admin', 'owner'])
|
||||
async def delete_customer(
|
||||
tenant_id: UUID = Path(...),
|
||||
customer_id: UUID = Path(...),
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
orders_service: OrdersService = Depends(get_orders_service),
|
||||
db = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Delete a customer (Admin+ only, GDPR-compliant soft delete)
|
||||
Removes PII while maintaining referential integrity
|
||||
"""
|
||||
try:
|
||||
customer = await orders_service.customer_repo.get(db, customer_id, tenant_id)
|
||||
if not customer:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Customer not found"
|
||||
)
|
||||
|
||||
# Capture customer data before deletion (for audit trail)
|
||||
# Note: This is anonymized after retention period in compliance with GDPR
|
||||
customer_data = {
|
||||
"customer_code": customer.customer_code,
|
||||
"customer_name": customer.customer_name,
|
||||
"email": customer.email,
|
||||
"phone": customer.phone,
|
||||
"business_type": customer.business_type if hasattr(customer, 'business_type') else None
|
||||
}
|
||||
|
||||
await orders_service.customer_repo.delete(db, customer_id, tenant_id)
|
||||
|
||||
# Commit the transaction to persist deletion
|
||||
await db.commit()
|
||||
|
||||
# Log HIGH severity audit event for customer deletion (GDPR compliance)
|
||||
try:
|
||||
await audit_logger.log_deletion(
|
||||
db_session=db,
|
||||
tenant_id=str(tenant_id),
|
||||
user_id=current_user["user_id"],
|
||||
resource_type="customer",
|
||||
resource_id=str(customer_id),
|
||||
resource_data=customer_data,
|
||||
description=f"Admin {current_user.get('email', 'unknown')} deleted customer {customer_data['customer_code']} (GDPR-compliant soft delete)",
|
||||
endpoint=f"/customers/{customer_id}",
|
||||
method="DELETE",
|
||||
severity=AuditSeverity.HIGH.value
|
||||
)
|
||||
except Exception as audit_error:
|
||||
logger.warning("Failed to log audit event", error=str(audit_error))
|
||||
|
||||
logger.info("Customer deleted successfully (GDPR-compliant)",
|
||||
customer_id=str(customer_id),
|
||||
tenant_id=str(tenant_id),
|
||||
user_id=current_user["user_id"])
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Error deleting customer",
|
||||
customer_id=str(customer_id),
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to delete customer"
|
||||
)
|
||||
Reference in New Issue
Block a user