package customerverification import ( "errors" "net/http" "github.com/gin-gonic/gin" "github.com/google/uuid" "backend/internal/platform/middleware" ) type Handler struct { service *Service } func NewHandler(service *Service) *Handler { return &Handler{service: service} } type verificationResponse struct { ID uuid.UUID `json:"id"` UserID uuid.UUID `json:"user_id"` Status string `json:"status"` AdminNote string `json:"admin_note"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` } func toResponse(v *CustomerVerification) verificationResponse { return verificationResponse{ ID: v.ID, UserID: v.UserID, Status: v.Status, AdminNote: v.AdminNote, CreatedAt: v.CreatedAt.Format("2006-01-02T15:04:05Z07:00"), UpdatedAt: v.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"), } } // Submit lets a logged-in customer upload their ID document (front + back) // for review. Whether this step is required at all is decided by the admin // (site.Settings.CustomerVerificationRequired) and enforced at checkout, // not here -- a customer may always submit early. func (h *Handler) Submit(c *gin.Context) { userID, ok := middleware.GetUserID(c) if !ok { c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) return } frontFile, frontHeader, err := c.Request.FormFile("front") if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "missing 'front' file"}) return } defer frontFile.Close() backFile, backHeader, err := c.Request.FormFile("back") if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "missing 'back' file"}) return } defer backFile.Close() v, err := h.service.Submit(c.Request.Context(), userID, DocumentInput{Reader: frontFile, ContentType: frontHeader.Header.Get("Content-Type")}, DocumentInput{Reader: backFile, ContentType: backHeader.Header.Get("Content-Type")}, ) if err != nil { if errors.Is(err, ErrUnsupportedType) { c.JSON(http.StatusUnsupportedMediaType, gin.H{"error": "documents must be JPEG or PNG images"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to submit verification"}) return } c.JSON(http.StatusCreated, toResponse(v)) } // Me returns the caller's own verification status, or 404 if they haven't // submitted anything yet (not an error -- the storefront treats this as // "not started"). func (h *Handler) Me(c *gin.Context) { userID, ok := middleware.GetUserID(c) if !ok { c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) return } v, err := h.service.GetByUser(c.Request.Context(), userID) if err != nil { if errors.Is(err, ErrNotFound) { c.JSON(http.StatusNotFound, gin.H{"error": "no verification submitted"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load verification"}) return } c.JSON(http.StatusOK, toResponse(v)) } // MyDocument streams the caller's own submitted document back to them // (e.g. so the storefront can show what was uploaded). func (h *Handler) MyDocument(c *gin.Context) { userID, ok := middleware.GetUserID(c) if !ok { c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) return } v, err := h.service.GetByUser(c.Request.Context(), userID) if err != nil { if errors.Is(err, ErrNotFound) { c.JSON(http.StatusNotFound, gin.H{"error": "no verification submitted"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load verification"}) return } h.serveDocument(c, v) } // ListAdmin lists submissions, optionally filtered by ?status=pending. func (h *Handler) ListAdmin(c *gin.Context) { list, err := h.service.List(c.Request.Context(), c.Query("status")) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list verifications"}) return } resp := make([]verificationResponse, 0, len(list)) for _, v := range list { resp = append(resp, toResponse(v)) } c.JSON(http.StatusOK, gin.H{"verifications": resp}) } func (h *Handler) GetAdmin(c *gin.Context) { id, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"}) return } v, err := h.service.Get(c.Request.Context(), id) if err != nil { if errors.Is(err, ErrNotFound) { c.JSON(http.StatusNotFound, gin.H{"error": "verification not found"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load verification"}) return } c.JSON(http.StatusOK, toResponse(v)) } // AdminDocument streams a specific submission's document (side=front|back) // to the admin for review. func (h *Handler) AdminDocument(c *gin.Context) { id, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"}) return } v, err := h.service.Get(c.Request.Context(), id) if err != nil { if errors.Is(err, ErrNotFound) { c.JSON(http.StatusNotFound, gin.H{"error": "verification not found"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load verification"}) return } h.serveDocument(c, v) } func (h *Handler) serveDocument(c *gin.Context, v *CustomerVerification) { path, err := h.service.DocumentPath(v, c.Param("side")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.File(path) } type reviewRequest struct { Status string `json:"status" binding:"required"` AdminNote string `json:"admin_note"` } func (h *Handler) Review(c *gin.Context) { id, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"}) return } var req reviewRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "validation error", "details": err.Error()}) return } if !ValidStatuses[req.Status] { c.JSON(http.StatusBadRequest, gin.H{"error": "status must be one of: pending, approved, rejected"}) return } v, err := h.service.Review(c.Request.Context(), id, req.Status, req.AdminNote) if err != nil { if errors.Is(err, ErrNotFound) { c.JSON(http.StatusNotFound, gin.H{"error": "verification not found"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to review verification"}) return } c.JSON(http.StatusOK, toResponse(v)) }