from datetime import datetime, timezone from pathlib import Path from uuid import UUID from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status from fastapi.responses import FileResponse from pydantic import BaseModel, Field from sqlalchemy.orm import Session from .auth import current_user from .config import settings from .database import get_db from .kyc_constants import ALLOWED_UPLOAD_EXTENSIONS, KYC_DOCUMENT_TYPES from .kyc_service import ensure_kyc_application, refresh_application_status, serialize_kyc, upload_root from .kyc_validators import validate_gst, validate_pan from .models import KycApplication, KycDocument, UserAccount router = APIRouter(prefix="/api/v1/kyc", tags=["kyc"]) class KycDetailsRequest(BaseModel): pan_number: str = Field(min_length=10, max_length=10) gst_number: str = Field(min_length=15, max_length=15) @router.get("/template") def download_template(_: UserAccount = Depends(current_user)) -> FileResponse: template = Path(settings.kyc_template_path) if not template.is_file(): raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="KYC template not found") return FileResponse( template, media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document", filename="KYC Form - Pvt Ltd Client.docx", ) @router.get("/me") def get_my_kyc(db: Session = Depends(get_db), user: UserAccount = Depends(current_user)) -> dict: application = ensure_kyc_application(db, user) return serialize_kyc(application) @router.put("/me") def update_my_kyc( payload: KycDetailsRequest, db: Session = Depends(get_db), user: UserAccount = Depends(current_user), ) -> dict: application = ensure_kyc_application(db, user) if application.status == "approved": raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Approved KYC cannot be edited") try: application.pan_number = validate_pan(payload.pan_number) application.gst_number = validate_gst(payload.gst_number) except ValueError as exc: raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc)) from exc if application.status == "not_started": application.status = "in_progress" db.commit() db.refresh(application) return serialize_kyc(application) @router.post("/documents/{document_type}") async def upload_document( document_type: str, file: UploadFile = File(...), db: Session = Depends(get_db), user: UserAccount = Depends(current_user), ) -> dict: if document_type not in KYC_DOCUMENT_TYPES: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid document type") application = ensure_kyc_application(db, user) if application.status == "approved": raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Approved KYC cannot be edited") if not application.pan_number or not application.gst_number: raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Enter PAN and GST before uploading documents", ) filename = file.filename or "" extension = Path(filename).suffix.lower() if extension not in ALLOWED_UPLOAD_EXTENSIONS: raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Allowed file types: PDF, PNG, JPG, DOC, DOCX", ) content = await file.read() if not content: raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="File is empty") if len(content) > settings.max_upload_bytes: raise HTTPException(status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE, detail="File exceeds 10 MB limit") document = next(doc for doc in application.documents if doc.document_type == document_type) root = upload_root(settings.upload_dir) stored_name = f"{application.id}_{document_type}{extension}" stored_path = root / stored_name stored_path.write_bytes(content) document.original_filename = filename document.stored_path = str(stored_path) document.status = "pending" document.review_note = None document.uploaded_at = datetime.now(timezone.utc) document.reviewed_at = None if application.status in {"not_started", "in_progress", "rejected"}: application.status = "in_progress" db.commit() db.refresh(application) return serialize_kyc(application) @router.post("/submit") def submit_kyc(db: Session = Depends(get_db), user: UserAccount = Depends(current_user)) -> dict: application = ensure_kyc_application(db, user) if application.status == "approved": return serialize_kyc(application) if not application.pan_number or not application.gst_number: raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="PAN and GST are required") missing = [doc.document_type for doc in application.documents if doc.status == "missing"] if missing: raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Upload all required documents before submitting", ) application.status = "submitted" application.submitted_at = datetime.now(timezone.utc) db.commit() db.refresh(application) return serialize_kyc(application)