from __future__ import annotations from datetime import UTC, datetime, timedelta from fastapi import APIRouter, Depends, HTTPException from fastapi.responses import JSONResponse from sqlalchemy import func, or_, select from sqlalchemy.orm import Session from app.api.deps import get_db, require_operations_manager from app.core.config import get_settings from app.models.booking import Booking from app.models.customer import Customer from app.schemas import ( CurrentUser, CustomerAnonymizeRequest, CustomerAnonymizeResult, PrivacyRetentionOut, ) from app.services.audit import record_audit_event router = APIRouter(prefix="/api/v1/privacy", tags=["privacy"]) settings = get_settings() def _retention_cutoff() -> datetime: return datetime.now(UTC) - timedelta(days=settings.privacy_minimum_booking_retention_days) def _customer_is_eligible(db: Session, customer_id) -> bool: blocking = db.scalar( select(func.count()) .select_from(Booking) .where( Booking.customer_id == customer_id, or_( Booking.status.in_(("reserved", "active")), Booking.ends_at > _retention_cutoff(), ), ) ) return not blocking @router.get("/retention", response_model=PrivacyRetentionOut) def retention_status( db: Session = Depends(get_db), _user: CurrentUser = Depends(require_operations_manager), ) -> PrivacyRetentionOut: customers = db.scalars(select(Customer)).all() return PrivacyRetentionOut( minimum_booking_retention_days=settings.privacy_minimum_booking_retention_days, audit_retention_days=settings.privacy_audit_retention_days, customers_total=len(customers), customers_anonymized=sum(customer.anonymized_at is not None for customer in customers), customers_eligible=sum( customer.anonymized_at is None and _customer_is_eligible(db, customer.id) for customer in customers ), ) @router.get("/customers/{public_ref}/export") def export_customer_data( public_ref: str, db: Session = Depends(get_db), actor: CurrentUser = Depends(require_operations_manager), ) -> JSONResponse: customer = db.scalar(select(Customer).where(Customer.public_ref == public_ref)) if customer is None: raise HTTPException(status_code=404, detail="Customer not found") bookings = db.scalars( select(Booking).where(Booking.customer_id == customer.id).order_by(Booking.starts_at) ).all() payload = { "generated_at": datetime.now(UTC).isoformat(), "customer": { "public_ref": customer.public_ref, "first_name": customer.first_name, "last_name": customer.last_name, "email": customer.email, "phone": customer.phone, "postal_code": customer.postal_code, "city": customer.city, "date_of_birth": customer.date_of_birth.isoformat() if customer.date_of_birth else None, "anonymized_at": customer.anonymized_at.isoformat() if customer.anonymized_at else None, }, "bookings": [ { "public_ref": booking.public_ref, "starts_at": booking.starts_at.isoformat(), "ends_at": booking.ends_at.isoformat(), "status": booking.status, } for booking in bookings ], } record_audit_event( db, actor_type="user", actor_label=actor.display_name, action="privacy_customer_exported", entity_type="customer", entity_id=customer.id, metadata={"customer_ref": customer.public_ref, "booking_count": len(bookings)}, ) db.commit() return JSONResponse( payload, headers={"Content-Disposition": f'attachment; filename="{public_ref}-privacy.json"'}, ) @router.post("/customers/{public_ref}/anonymize", response_model=CustomerAnonymizeResult) def anonymize_customer( public_ref: str, body: CustomerAnonymizeRequest, db: Session = Depends(get_db), actor: CurrentUser = Depends(require_operations_manager), ) -> CustomerAnonymizeResult: customer = db.scalar( select(Customer).where(Customer.public_ref == public_ref).with_for_update() ) if customer is None: raise HTTPException(status_code=404, detail="Customer not found") if body.confirmation != public_ref: raise HTTPException( status_code=422, detail="Customer reference confirmation does not match" ) if customer.anonymized_at is not None: return CustomerAnonymizeResult( public_ref=public_ref, anonymized_at=customer.anonymized_at, status="already_anonymized", ) if not _customer_is_eligible(db, customer.id): raise HTTPException( status_code=409, detail="Customer has an active/recent booking within the minimum retention period", ) anonymized_at = datetime.now(UTC) customer.first_name = "Anoniem" customer.last_name = public_ref customer.email = None customer.phone = None customer.postal_code = None customer.city = None customer.date_of_birth = None customer.anonymized_at = anonymized_at record_audit_event( db, actor_type="user", actor_label=actor.display_name, action="privacy_customer_anonymized", entity_type="customer", entity_id=customer.id, before={"anonymized": False}, after={"anonymized": True}, metadata={"reason": body.reason, "customer_ref": public_ref}, ) db.commit() return CustomerAnonymizeResult( public_ref=public_ref, anonymized_at=anonymized_at, status="anonymized", )