133 lines
5.3 KiB
Python
133 lines
5.3 KiB
Python
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)
|