From 07b186c9864cd677a416ae4df2fc49c1099e94a5 Mon Sep 17 00:00:00 2001 From: ryan Date: Sat, 9 May 2026 01:01:25 +0700 Subject: [PATCH] Barcode generation with Boombuler --- go.mod | 1 + go.sum | 2 ++ internal/handler/table_handler.go | 49 ++++++++++++++++++++++++++- internal/handler/table_service.go | 1 + internal/pkg/qrcode/generator.go | 32 +++++++++++++++++ internal/processor/table_processor.go | 8 +++++ internal/router/router.go | 3 +- internal/service/table_service.go | 4 +++ 8 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 internal/pkg/qrcode/generator.go diff --git a/go.mod b/go.mod index e502de5..bd967f9 100644 --- a/go.mod +++ b/go.mod @@ -63,6 +63,7 @@ require ( require ( github.com/aws/aws-sdk-go v1.55.7 + github.com/boombuler/barcode v1.1.0 github.com/golang-jwt/jwt/v5 v5.2.3 github.com/redis/go-redis/v9 v9.19.0 github.com/sirupsen/logrus v1.9.3 diff --git a/go.sum b/go.sum index 67735e8..b1a6622 100644 --- a/go.sum +++ b/go.sum @@ -42,6 +42,8 @@ github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/boombuler/barcode v1.1.0 h1:ChaYjBR63fr4LFyGn8E8nt7dBSt3MiU3zMOZqFvVkHo= +github.com/boombuler/barcode v1.1.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= diff --git a/internal/handler/table_handler.go b/internal/handler/table_handler.go index e22aea9..c9eee19 100644 --- a/internal/handler/table_handler.go +++ b/internal/handler/table_handler.go @@ -5,8 +5,11 @@ import ( "apskel-pos-be/internal/constants" "apskel-pos-be/internal/contract" "apskel-pos-be/internal/logger" + "apskel-pos-be/internal/pkg/qrcode" "apskel-pos-be/internal/util" "apskel-pos-be/internal/validator" + "fmt" + "net/http" "strconv" "github.com/gin-gonic/gin" @@ -16,12 +19,14 @@ import ( type TableHandler struct { tableService TableService tableValidator *validator.TableValidator + baseURL string } -func NewTableHandler(tableService TableService, tableValidator *validator.TableValidator) *TableHandler { +func NewTableHandler(tableService TableService, tableValidator *validator.TableValidator, baseURL string) *TableHandler { return &TableHandler{ tableService: tableService, tableValidator: tableValidator, + baseURL: baseURL, } } @@ -286,3 +291,45 @@ func (h *TableHandler) GetOccupiedTables(c *gin.Context) { util.HandleResponse(c.Writer, c.Request, response, "TableHandler::GetOccupiedTables") } + +func (h *TableHandler) GenerateQRCode(c *gin.Context) { + ctx := c.Request.Context() + + id := c.Param("id") + tableID, err := uuid.Parse(id) + if err != nil { + logger.FromContext(ctx).WithError(err).Error("TableHandler::GenerateQRCode -> Invalid table ID") + validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid table ID") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "TableHandler::GenerateQRCode") + return + } + + token, err := h.tableService.GetTableToken(ctx, tableID) + if err != nil { + logger.FromContext(ctx).WithError(err).Error("TableHandler::GenerateQRCode -> table not found") + validationResponseError := contract.NewResponseError(constants.NotFoundErrorCode, constants.TableEntity, "Table not found") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "TableHandler::GenerateQRCode") + return + } + + selfOrderURL := fmt.Sprintf("%s/api/v1/self-order/table/%s", h.baseURL, token) + + size := 256 + if sizeStr := c.Query("size"); sizeStr != "" { + if s, err := strconv.Atoi(sizeStr); err == nil && s > 0 && s <= 1024 { + size = s + } + } + + pngBytes, err := qrcode.GeneratePNG(selfOrderURL, size) + if err != nil { + logger.FromContext(ctx).WithError(err).Error("TableHandler::GenerateQRCode -> QR generation failed") + validationResponseError := contract.NewResponseError(constants.InternalServerErrorCode, constants.TableEntity, "Failed to generate QR code") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "TableHandler::GenerateQRCode") + return + } + + c.Header("Content-Type", "image/png") + c.Header("Content-Disposition", fmt.Sprintf("inline; filename=\"table-%s-qr.png\"", tableID)) + c.Data(http.StatusOK, "image/png", pngBytes) +} diff --git a/internal/handler/table_service.go b/internal/handler/table_service.go index 067706c..5149ab9 100644 --- a/internal/handler/table_service.go +++ b/internal/handler/table_service.go @@ -17,4 +17,5 @@ type TableService interface { ReleaseTable(ctx context.Context, tableID uuid.UUID, req *contract.ReleaseTableRequest) *contract.Response GetAvailableTables(ctx context.Context, outletID uuid.UUID) *contract.Response GetOccupiedTables(ctx context.Context, outletID uuid.UUID) *contract.Response + GetTableToken(ctx context.Context, tableID uuid.UUID) (string, error) } diff --git a/internal/pkg/qrcode/generator.go b/internal/pkg/qrcode/generator.go new file mode 100644 index 0000000..c07b760 --- /dev/null +++ b/internal/pkg/qrcode/generator.go @@ -0,0 +1,32 @@ +package qrcode + +import ( + "bytes" + "image/png" + + "github.com/boombuler/barcode" + "github.com/boombuler/barcode/qr" +) + +func GeneratePNG(content string, size int) ([]byte, error) { + if size <= 0 { + size = 256 + } + + qrCode, err := qr.Encode(content, qr.M, qr.Auto) + if err != nil { + return nil, err + } + + qrCode, err = barcode.Scale(qrCode, size, size) + if err != nil { + return nil, err + } + + var buf bytes.Buffer + if err := png.Encode(&buf, qrCode); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} diff --git a/internal/processor/table_processor.go b/internal/processor/table_processor.go index 5cc872e..0adf23c 100644 --- a/internal/processor/table_processor.go +++ b/internal/processor/table_processor.go @@ -207,6 +207,14 @@ func (p *TableProcessor) GetOccupiedTables(ctx context.Context, outletID uuid.UU return responses, nil } +func (p *TableProcessor) GetTokenByID(ctx context.Context, id uuid.UUID) (string, error) { + table, err := p.tableRepo.GetByID(ctx, id) + if err != nil { + return "", err + } + return table.Token, nil +} + func (p *TableProcessor) mapTableToResponse(table *entities.Table) *models.TableResponse { response := &models.TableResponse{ ID: table.ID, diff --git a/internal/router/router.go b/internal/router/router.go index ea62af9..e8b2d95 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -70,7 +70,7 @@ func NewRouter(cfg *config.Config, healthHandler *handler.HealthHandler, authSer paymentMethodHandler: handler.NewPaymentMethodHandler(paymentMethodService, paymentMethodValidator), analyticsHandler: handler.NewAnalyticsHandler(analyticsService, transformer.NewTransformer()), reportHandler: handler.NewReportHandler(reportService, userService), - tableHandler: handler.NewTableHandler(tableService, tableValidator), + tableHandler: handler.NewTableHandler(tableService, tableValidator, cfg.Server.BaseUrl), unitHandler: handler.NewUnitHandler(unitService), ingredientHandler: handler.NewIngredientHandler(ingredientService), productRecipeHandler: handler.NewProductRecipeHandler(productRecipeService), @@ -323,6 +323,7 @@ func (r *Router) addAppRoutes(rg *gin.Engine) { tables.DELETE("/:id", r.tableHandler.Delete) tables.POST("/:id/occupy", r.tableHandler.OccupyTable) tables.POST("/:id/release", r.tableHandler.ReleaseTable) + tables.GET("/:id/qr", r.tableHandler.GenerateQRCode) } ingredients := protected.Group("/ingredients") diff --git a/internal/service/table_service.go b/internal/service/table_service.go index 47072f4..c374a80 100644 --- a/internal/service/table_service.go +++ b/internal/service/table_service.go @@ -152,3 +152,7 @@ func (s *TableServiceImpl) GetOccupiedTables(ctx context.Context, outletID uuid. return contract.BuildSuccessResponse(responses) } + +func (s *TableServiceImpl) GetTableToken(ctx context.Context, tableID uuid.UUID) (string, error) { + return s.tableProcessor.GetTokenByID(ctx, tableID) +}