Files
template-vitrine/backend/internal/modules/customerverification/handler.go
T
Xor290 919c807004
ci-api / test (push) Failing after 8m7s
ci-web / test (push) Failing after 5m5s
chore: build
2026-09-20 12:18:33 +02:00

213 lines
6.2 KiB
Go

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))
}