from __future__ import annotations from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy import select from sqlalchemy.orm import Session from app.api.deps import get_current_user, get_db, require_operations_manager from app.models.customer import Customer from app.models.data_quality import DataQualityIssue from app.models.vehicle import Vehicle from app.schemas import ( CurrentUser, DataQualityIssueDetailOut, DataQualityIssueOut, MergeCustomersRequest, MergeCustomersResult, ScanResultOut, ) from app.services.data_quality import defer_issue, merge_customers, reject_issue, run_scan router = APIRouter(prefix="/api/v1/data-quality", tags=["data-quality"]) def _to_out(issue: DataQualityIssue) -> DataQualityIssueOut: return DataQualityIssueOut( public_ref=issue.public_ref, rule_type=issue.rule_type, entity_type=issue.entity_type, entity_ref=issue.evidence_json.get("entity_ref", ""), severity=issue.severity, status=issue.status, evidence=issue.evidence_json, detected_at=issue.detected_at, resolved_at=issue.resolved_at, ) @router.get("/issues", response_model=list[DataQualityIssueOut]) def list_issues( status: str | None = Query(default=None), rule_type: str | None = Query(default=None), severity: str | None = Query(default=None), db: Session = Depends(get_db), _user: CurrentUser = Depends(get_current_user), ) -> list[DataQualityIssueOut]: stmt = select(DataQualityIssue).order_by(DataQualityIssue.detected_at.desc()) if status: stmt = stmt.where(DataQualityIssue.status == status) if rule_type: stmt = stmt.where(DataQualityIssue.rule_type == rule_type) if severity: stmt = stmt.where(DataQualityIssue.severity == severity) issues = db.scalars(stmt).all() return [_to_out(i) for i in issues] def _snapshot(entity_type: str, ref: str, db: Session) -> dict | None: if entity_type == "customer": obj = db.scalar(select(Customer).where(Customer.public_ref == ref)) if obj is None: return None return { "public_ref": obj.public_ref, "first_name": obj.first_name, "last_name": obj.last_name, "email": obj.email, "phone": obj.phone, "postal_code": obj.postal_code, "city": obj.city, } obj = db.scalar(select(Vehicle).where(Vehicle.public_ref == ref)) if obj is None: return None return { "public_ref": obj.public_ref, "make": obj.make, "model": obj.model, "location": obj.location, "operational_status": obj.operational_status, "odometer_km": obj.odometer_km, } @router.get("/issues/{public_ref}", response_model=DataQualityIssueDetailOut) def get_issue( public_ref: str, db: Session = Depends(get_db), _user: CurrentUser = Depends(get_current_user), ) -> DataQualityIssueDetailOut: issue = db.scalar(select(DataQualityIssue).where(DataQualityIssue.public_ref == public_ref)) if issue is None: raise HTTPException(status_code=404, detail="Data quality issue not found") base = _to_out(issue) related_refs = issue.evidence_json.get("related_refs", []) related_entity_type = ( "customer" if issue.rule_type == "possible_duplicate_customer" else "vehicle" ) return DataQualityIssueDetailOut( **base.model_dump(), entity_snapshot=_snapshot(issue.entity_type, base.entity_ref, db), related_snapshots=[ snap for ref in related_refs if (snap := _snapshot(related_entity_type, ref, db)) is not None ], ) @router.post("/issues/{public_ref}/defer", response_model=DataQualityIssueOut) def defer( public_ref: str, db: Session = Depends(get_db), user: CurrentUser = Depends(get_current_user), ) -> DataQualityIssueOut: issue = defer_issue(db, public_ref, user) return _to_out(issue) @router.post("/issues/{public_ref}/reject", response_model=DataQualityIssueOut) def reject( public_ref: str, db: Session = Depends(get_db), user: CurrentUser = Depends(get_current_user), ) -> DataQualityIssueOut: issue = reject_issue(db, public_ref, user) return _to_out(issue) @router.post("/issues/{public_ref}/merge-customers", response_model=MergeCustomersResult) def merge( public_ref: str, body: MergeCustomersRequest, db: Session = Depends(get_db), user: CurrentUser = Depends(require_operations_manager), ) -> MergeCustomersResult: result = merge_customers(db, public_ref, body.survivor_ref, body.field_overrides, user) return MergeCustomersResult(**result) @router.post("/scan", response_model=ScanResultOut) def scan( db: Session = Depends(get_db), _user: CurrentUser = Depends(require_operations_manager), ) -> ScanResultOut: result = run_scan(db) return ScanResultOut(created=result.created)