213 lines
6.2 KiB
Go
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))
|
|
}
|