from fastapi import APIRouter, Depends, HTTPException, Query, status from fastapi.responses import FileResponse from sqlalchemy import or_ from sqlalchemy.orm import Session from app.api.deps import require_roles from app.core.security import generate_public_token, hash_token from app.db.session import get_db from app.models import AdminUser, Certificate, CertificateAccessToken, Learner, ProjectCourse from app.schemas.certificate import CertificateCreate, CertificateOut from app.services.certificate_number import build_certificate_no from app.services.logs import log_action from app.services.pdf import render_certificate_pdf router = APIRouter() def _create_token(db: Session, certificate_id: int, token_type: str) -> int: raw_token = generate_public_token() token = CertificateAccessToken( certificate_id=certificate_id, token_hash=hash_token(raw_token), token_value=raw_token, token_type=token_type, ) db.add(token) db.flush() return token.id @router.get("", response_model=list[CertificateOut]) def list_certificates( keyword: str | None = Query(default=None), status_value: str | None = Query(default=None, alias="status"), db: Session = Depends(get_db), _: AdminUser = Depends(require_roles("system_admin", "certificate_admin", "readonly")), ) -> list[Certificate]: query = db.query(Certificate).order_by(Certificate.id.desc()) if keyword: like = f"%{keyword}%" query = query.filter( or_( Certificate.certificate_no.like(like), Certificate.certificate_name.like(like), Certificate.project_code.like(like), Certificate.course_name.like(like), Certificate.stage_name.like(like), ) ) if status_value: query = query.filter(Certificate.status == status_value) return query.limit(100).all() @router.post("", response_model=CertificateOut, status_code=status.HTTP_201_CREATED) def create_certificate( payload: CertificateCreate, db: Session = Depends(get_db), admin: AdminUser = Depends(require_roles("system_admin", "certificate_admin")), ) -> Certificate: learner = db.get(Learner, payload.learner_id) if not learner: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Learner not found") project = db.query(ProjectCourse).filter(ProjectCourse.code == payload.project_code, ProjectCourse.status == "active").first() if not project: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Project code is inactive or missing") certificate = Certificate( learner_id=payload.learner_id, project_code=payload.project_code, certificate_no="PENDING", certificate_name=project.name, class_name=payload.class_name, course_name=payload.course_name or project.default_course_name or project.name, stage_name=payload.stage_name or project.default_stage_name, issue_date=payload.issue_date, issuer_name=payload.issuer_name or project.default_issuer_name, remark=payload.remark, ) db.add(certificate) db.flush() certificate.certificate_no = build_certificate_no(certificate.id, certificate.project_code, certificate.issue_date) certificate.public_token_id = _create_token(db, certificate.id, "public_link") certificate.qr_token_id = _create_token(db, certificate.id, "qr_verify") log_action(db, admin, "create_certificate", "certificate", certificate.id, {"certificate_no": certificate.certificate_no, "learner_name": learner.current_name, "project_code": certificate.project_code, "issue_date": certificate.issue_date}) db.commit() db.refresh(certificate) return certificate @router.get("/{certificate_id}", response_model=CertificateOut) def get_certificate( certificate_id: int, db: Session = Depends(get_db), _: AdminUser = Depends(require_roles("system_admin", "certificate_admin", "readonly")), ) -> Certificate: certificate = db.get(Certificate, certificate_id) if not certificate: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Certificate not found") return certificate @router.get("/{certificate_id}/preview") def preview_certificate( certificate_id: int, db: Session = Depends(get_db), _: AdminUser = Depends(require_roles("system_admin", "certificate_admin", "readonly")), ) -> dict[str, object]: certificate = db.get(Certificate, certificate_id) if not certificate: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Certificate not found") learner = db.get(Learner, certificate.learner_id) public_token = db.get(CertificateAccessToken, certificate.public_token_id) if certificate.public_token_id else None qr_token = db.get(CertificateAccessToken, certificate.qr_token_id) if certificate.qr_token_id else None return { "certificate_no": certificate.certificate_no, "learner_name": learner.current_name if learner else "", "certificate_name": certificate.certificate_name, "project_code": certificate.project_code, "course_name": certificate.course_name, "stage_name": certificate.stage_name, "issue_date": certificate.issue_date.isoformat(), "issuer_name": certificate.issuer_name, "status": certificate.status, "public_url": f"/cert/{public_token.token_value}" if public_token else "", "verify_url": f"/verify/{qr_token.token_value}" if qr_token else "", } @router.get("/{certificate_id}/download") def download_certificate( certificate_id: int, db: Session = Depends(get_db), _: AdminUser = Depends(require_roles("system_admin", "certificate_admin")), ) -> FileResponse: certificate = db.get(Certificate, certificate_id) if not certificate: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Certificate not found") if certificate.status != "valid": raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Voided certificate cannot download normal PDF") learner = db.get(Learner, certificate.learner_id) token = db.get(CertificateAccessToken, certificate.qr_token_id or certificate.public_token_id) project = db.query(ProjectCourse).filter(ProjectCourse.code == certificate.project_code).first() if not learner or not token: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Certificate data is incomplete") pdf_path = render_certificate_pdf(certificate, learner, project, token) db.commit() return FileResponse(pdf_path, media_type="application/pdf", filename=f"{certificate.certificate_no}.pdf") @router.post("/{certificate_id}/void", response_model=CertificateOut) def void_certificate( certificate_id: int, db: Session = Depends(get_db), admin: AdminUser = Depends(require_roles("system_admin", "certificate_admin")), ) -> Certificate: certificate = db.get(Certificate, certificate_id) if not certificate: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Certificate not found") certificate.status = "voided" certificate.pdf_status = "not_generated" certificate.pdf_file_path = None learner = db.get(Learner, certificate.learner_id) log_action(db, admin, "void_certificate", "certificate", certificate.id, {"certificate_no": certificate.certificate_no, "learner_name": learner.current_name if learner else "", "previous_status": "valid", "status": "voided"}) db.commit() db.refresh(certificate) return certificate @router.post("/{certificate_id}/token/regenerate", response_model=CertificateOut) def regenerate_public_token( certificate_id: int, db: Session = Depends(get_db), admin: AdminUser = Depends(require_roles("system_admin", "certificate_admin")), ) -> Certificate: certificate = db.get(Certificate, certificate_id) if not certificate: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Certificate not found") if certificate.public_token_id: old_token = db.get(CertificateAccessToken, certificate.public_token_id) if old_token: old_token.status = "revoked" certificate.public_token_id = _create_token(db, certificate.id, "public_link") learner = db.get(Learner, certificate.learner_id) log_action( db, admin, "regenerate_public_token", "certificate", certificate.id, {"certificate_no": certificate.certificate_no, "learner_name": learner.current_name if learner else ""}, ) db.commit() db.refresh(certificate) return certificate