package handler import ( "encoding/base64" "fmt" "io" "net/http" "time" "go-backend-template/config" "go-backend-template/internal/constants" "go-backend-template/internal/contract" "go-backend-template/internal/logger" "go-backend-template/internal/util" "github.com/gin-gonic/gin" ) type DukcapilHandler struct { dukcapilService DukcapilService config *config.Config } func NewDukcapilHandler(dukcapilService DukcapilService, cfg *config.Config) *DukcapilHandler { return &DukcapilHandler{ dukcapilService: dukcapilService, config: cfg, } } // FaceMatch handles POST /api/v1/dukcapil/face-match (1:N face recognition). // Accepts only an image file via multipart form. All other parameters are // generated or retrieved from configuration. func (h *DukcapilHandler) FaceMatch(c *gin.Context) { // Parse multipart form file, err := c.FormFile("image") if err != nil { logger.FromContext(c.Request.Context()).WithError(err).Error("DukcapilHandler::FaceMatch -> failed to get image file") h.sendValidationError(c, "image file is required", constants.MissingFieldErrorCode) return } // Open the uploaded file src, err := file.Open() if err != nil { logger.FromContext(c.Request.Context()).WithError(err).Error("DukcapilHandler::FaceMatch -> failed to open image file") h.sendValidationError(c, "failed to read image file", constants.MalformedFieldErrorCode) return } defer src.Close() // Read file content imageBytes, err := io.ReadAll(src) if err != nil { logger.FromContext(c.Request.Context()).WithError(err).Error("DukcapilHandler::FaceMatch -> failed to read image bytes") h.sendValidationError(c, "failed to read image file", constants.MalformedFieldErrorCode) return } // Convert to base64 imageBase64 := base64.StdEncoding.EncodeToString(imageBytes) // Generate transaction_id (timestamp-based random ID) transactionID := fmt.Sprintf("TXN%d", time.Now().UnixNano()) // Build request with config values req := &contract.FaceMatchRequest{ TransactionID: transactionID, TransactionSource: h.config.Dukcapil.TransactionSource, Threshold: h.config.Dukcapil.Threshold, Image: imageBase64, IP: h.config.Dukcapil.DefaultIP, } res, err := h.dukcapilService.FaceMatch(c.Request.Context(), req) if err != nil { logger.FromContext(c.Request.Context()).WithError(err).Error("DukcapilHandler::FaceMatch -> upstream call failed") c.JSON(http.StatusBadGateway, &contract.ErrorResponse{ Error: "upstream_error", Message: err.Error(), Code: http.StatusBadGateway, Details: map[string]interface{}{"entity": constants.DukcapilHandlerEntity}, }) return } logger.FromContext(c.Request.Context()).Infof("DukcapilHandler::FaceMatch -> tid=%s errorCode=%s matches=%d", res.TID, res.ErrorCode, len(res.Matches)) util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(res), "DukcapilHandler::FaceMatch") } func (h *DukcapilHandler) sendValidationError(c *gin.Context, message, code string) { c.JSON(http.StatusBadRequest, &contract.ErrorResponse{ Error: "validation_error", Message: message, Code: http.StatusBadRequest, Details: map[string]interface{}{ "error_code": code, "entity": constants.DukcapilHandlerEntity, }, }) }