package handler import ( "io" "mime/multipart" "strings" "github.com/gin-gonic/gin" "github.com/google/uuid" "apskel-pos-be/internal/appcontext" "apskel-pos-be/internal/constants" "apskel-pos-be/internal/contract" "apskel-pos-be/internal/models" "apskel-pos-be/internal/service" "apskel-pos-be/internal/util" ) // EnakGameAdminHandler serves /marketing/enakgame (docs/rfc-enakgame.md ยง11). Every // request works on the admin's own organization. type EnakGameAdminHandler struct { service service.EnakGameAdminService } func NewEnakGameAdminHandler(s service.EnakGameAdminService) *EnakGameAdminHandler { return &EnakGameAdminHandler{service: s} } // voucherCodeUploadLimit caps a CSV of codes: 50 000 lines of up to 255 characters. const voucherCodeUploadLimit = 16 << 20 // pathID reads a UUID path parameter, answering 400 when it is not one. func pathID(c *gin.Context, name, method string) (uuid.UUID, bool) { id, err := uuid.Parse(c.Param(name)) if err != nil { util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "invalid "+name), }), method) return uuid.Nil, false } return id, true } // rawBody reads the request body, answering 400 when it cannot. func rawBody(c *gin.Context, method string) ([]byte, bool) { body, err := c.GetRawData() if err != nil { util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "cannot read the request body"), }), method) return nil, false } return body, true } func bindQuery(c *gin.Context, q any, method string) bool { if err := c.ShouldBindQuery(q); err != nil { util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, err.Error()), }), method) return false } return true } // CreateGame is POST /marketing/enakgame/games. func (h *EnakGameAdminHandler) CreateGame(c *gin.Context) { const method = "EnakGameAdminHandler::CreateGame" body, ok := rawBody(c, method) if !ok { return } ctx := c.Request.Context() util.HandleResponse(c.Writer, c.Request, h.service.CreateGame(ctx, appcontext.FromGinContext(ctx), body), method) } // ListGames is GET /marketing/enakgame/games?status=&search=&page=&limit=. func (h *EnakGameAdminHandler) ListGames(c *gin.Context) { const method = "EnakGameAdminHandler::ListGames" var q models.EnakGameListQuery if !bindQuery(c, &q, method) { return } ctx := c.Request.Context() util.HandleResponse(c.Writer, c.Request, h.service.ListGames(ctx, appcontext.FromGinContext(ctx), q), method) } // GetGame is GET /marketing/enakgame/games/:id. func (h *EnakGameAdminHandler) GetGame(c *gin.Context) { const method = "EnakGameAdminHandler::GetGame" id, ok := pathID(c, "id", method) if !ok { return } ctx := c.Request.Context() util.HandleResponse(c.Writer, c.Request, h.service.GetGame(ctx, appcontext.FromGinContext(ctx), id), method) } // UpdateGame is PUT /marketing/enakgame/games/:id. func (h *EnakGameAdminHandler) UpdateGame(c *gin.Context) { const method = "EnakGameAdminHandler::UpdateGame" id, ok := pathID(c, "id", method) if !ok { return } body, ok := rawBody(c, method) if !ok { return } ctx := c.Request.Context() util.HandleResponse(c.Writer, c.Request, h.service.UpdateGame(ctx, appcontext.FromGinContext(ctx), id, body), method) } // SetGameStatus is PUT /marketing/enakgame/games/:id/status. func (h *EnakGameAdminHandler) SetGameStatus(c *gin.Context) { const method = "EnakGameAdminHandler::SetGameStatus" id, ok := pathID(c, "id", method) if !ok { return } body, ok := rawBody(c, method) if !ok { return } ctx := c.Request.Context() util.HandleResponse(c.Writer, c.Request, h.service.SetGameStatus(ctx, appcontext.FromGinContext(ctx), id, body), method) } // CreateRewardConfig is POST /marketing/enakgame/games/:id/reward-configs. func (h *EnakGameAdminHandler) CreateRewardConfig(c *gin.Context) { const method = "EnakGameAdminHandler::CreateRewardConfig" id, ok := pathID(c, "id", method) if !ok { return } body, ok := rawBody(c, method) if !ok { return } ctx := c.Request.Context() util.HandleResponse(c.Writer, c.Request, h.service.CreateRewardConfig(ctx, appcontext.FromGinContext(ctx), id, body), method) } // ListRewardConfigs is GET /marketing/enakgame/games/:id/reward-configs. func (h *EnakGameAdminHandler) ListRewardConfigs(c *gin.Context) { const method = "EnakGameAdminHandler::ListRewardConfigs" id, ok := pathID(c, "id", method) if !ok { return } ctx := c.Request.Context() util.HandleResponse(c.Writer, c.Request, h.service.ListRewardConfigs(ctx, appcontext.FromGinContext(ctx), id), method) } // ActivateRewardConfig is POST /marketing/enakgame/reward-configs/:id/activate. func (h *EnakGameAdminHandler) ActivateRewardConfig(c *gin.Context) { const method = "EnakGameAdminHandler::ActivateRewardConfig" id, ok := pathID(c, "id", method) if !ok { return } body, ok := rawBody(c, method) if !ok { return } ctx := c.Request.Context() util.HandleResponse(c.Writer, c.Request, h.service.ActivateRewardConfig(ctx, appcontext.FromGinContext(ctx), id, body), method) } // CreateBudget is POST /marketing/enakgame/budgets. func (h *EnakGameAdminHandler) CreateBudget(c *gin.Context) { const method = "EnakGameAdminHandler::CreateBudget" body, ok := rawBody(c, method) if !ok { return } ctx := c.Request.Context() util.HandleResponse(c.Writer, c.Request, h.service.CreateBudget(ctx, appcontext.FromGinContext(ctx), body), method) } // ListBudgets is GET /marketing/enakgame/budgets?scope=&page=&limit=. func (h *EnakGameAdminHandler) ListBudgets(c *gin.Context) { const method = "EnakGameAdminHandler::ListBudgets" var q models.GameBudgetListQuery if !bindQuery(c, &q, method) { return } ctx := c.Request.Context() util.HandleResponse(c.Writer, c.Request, h.service.ListBudgets(ctx, appcontext.FromGinContext(ctx), q), method) } // GetBudget is GET /marketing/enakgame/budgets/:id. func (h *EnakGameAdminHandler) GetBudget(c *gin.Context) { const method = "EnakGameAdminHandler::GetBudget" id, ok := pathID(c, "id", method) if !ok { return } ctx := c.Request.Context() util.HandleResponse(c.Writer, c.Request, h.service.GetBudget(ctx, appcontext.FromGinContext(ctx), id), method) } // UpdateBudget is PUT /marketing/enakgame/budgets/:id. func (h *EnakGameAdminHandler) UpdateBudget(c *gin.Context) { const method = "EnakGameAdminHandler::UpdateBudget" id, ok := pathID(c, "id", method) if !ok { return } body, ok := rawBody(c, method) if !ok { return } ctx := c.Request.Context() util.HandleResponse(c.Writer, c.Request, h.service.UpdateBudget(ctx, appcontext.FromGinContext(ctx), id, body), method) } // DeleteBudget is DELETE /marketing/enakgame/budgets/:id. func (h *EnakGameAdminHandler) DeleteBudget(c *gin.Context) { const method = "EnakGameAdminHandler::DeleteBudget" id, ok := pathID(c, "id", method) if !ok { return } ctx := c.Request.Context() util.HandleResponse(c.Writer, c.Request, h.service.DeleteBudget(ctx, appcontext.FromGinContext(ctx), id), method) } // BudgetMetrics is GET /marketing/enakgame/budgets/:id/metrics. func (h *EnakGameAdminHandler) BudgetMetrics(c *gin.Context) { const method = "EnakGameAdminHandler::BudgetMetrics" id, ok := pathID(c, "id", method) if !ok { return } ctx := c.Request.Context() util.HandleResponse(c.Writer, c.Request, h.service.BudgetMetrics(ctx, appcontext.FromGinContext(ctx), id), method) } // BudgetRecommendation is GET /marketing/enakgame/budgets/:id/recommendation. func (h *EnakGameAdminHandler) BudgetRecommendation(c *gin.Context) { const method = "EnakGameAdminHandler::BudgetRecommendation" id, ok := pathID(c, "id", method) if !ok { return } ctx := c.Request.Context() util.HandleResponse(c.Writer, c.Request, h.service.BudgetRecommendation(ctx, appcontext.FromGinContext(ctx), id), method) } // AcceptBudgetRecommendation is POST /marketing/enakgame/budgets/:id/recommendation/accept. func (h *EnakGameAdminHandler) AcceptBudgetRecommendation(c *gin.Context) { const method = "EnakGameAdminHandler::AcceptBudgetRecommendation" id, ok := pathID(c, "id", method) if !ok { return } body, ok := rawBody(c, method) if !ok { return } ctx := c.Request.Context() util.HandleResponse(c.Writer, c.Request, h.service.AcceptBudgetRecommendation(ctx, appcontext.FromGinContext(ctx), id, body), method) } // GameAnalytics is GET /marketing/enakgame/analytics/games?from=&to=&game_id=. func (h *EnakGameAdminHandler) GameAnalytics(c *gin.Context) { const method = "EnakGameAdminHandler::GameAnalytics" var q models.EnakGameAnalyticsQuery if !bindQuery(c, &q, method) { return } ctx := c.Request.Context() util.HandleResponse(c.Writer, c.Request, h.service.GameAnalytics(ctx, appcontext.FromGinContext(ctx), q), method) } // EconomyAnalytics is GET /marketing/enakgame/analytics/economy?from=&to=. func (h *EnakGameAdminHandler) EconomyAnalytics(c *gin.Context) { const method = "EnakGameAdminHandler::EconomyAnalytics" var q models.EnakGameAnalyticsQuery if !bindQuery(c, &q, method) { return } ctx := c.Request.Context() util.HandleResponse(c.Writer, c.Request, h.service.EconomyAnalytics(ctx, appcontext.FromGinContext(ctx), q), method) } // CreateVoucher is POST /marketing/vouchers. func (h *EnakGameAdminHandler) CreateVoucher(c *gin.Context) { const method = "EnakGameAdminHandler::CreateVoucher" body, ok := rawBody(c, method) if !ok { return } ctx := c.Request.Context() util.HandleResponse(c.Writer, c.Request, h.service.CreateVoucher(ctx, appcontext.FromGinContext(ctx), body), method) } // ListVouchers is GET /marketing/vouchers?status=&search=&page=&limit=. func (h *EnakGameAdminHandler) ListVouchers(c *gin.Context) { const method = "EnakGameAdminHandler::ListVouchers" var q models.VoucherListQuery if !bindQuery(c, &q, method) { return } ctx := c.Request.Context() util.HandleResponse(c.Writer, c.Request, h.service.ListVouchers(ctx, appcontext.FromGinContext(ctx), q), method) } // GetVoucher is GET /marketing/vouchers/:id. func (h *EnakGameAdminHandler) GetVoucher(c *gin.Context) { const method = "EnakGameAdminHandler::GetVoucher" id, ok := pathID(c, "id", method) if !ok { return } ctx := c.Request.Context() util.HandleResponse(c.Writer, c.Request, h.service.GetVoucher(ctx, appcontext.FromGinContext(ctx), id), method) } // UpdateVoucher is PUT /marketing/vouchers/:id. func (h *EnakGameAdminHandler) UpdateVoucher(c *gin.Context) { const method = "EnakGameAdminHandler::UpdateVoucher" id, ok := pathID(c, "id", method) if !ok { return } body, ok := rawBody(c, method) if !ok { return } ctx := c.Request.Context() util.HandleResponse(c.Writer, c.Request, h.service.UpdateVoucher(ctx, appcontext.FromGinContext(ctx), id, body), method) } // SetVoucherStatus is PUT /marketing/vouchers/:id/status. func (h *EnakGameAdminHandler) SetVoucherStatus(c *gin.Context) { const method = "EnakGameAdminHandler::SetVoucherStatus" id, ok := pathID(c, "id", method) if !ok { return } body, ok := rawBody(c, method) if !ok { return } ctx := c.Request.Context() util.HandleResponse(c.Writer, c.Request, h.service.SetVoucherStatus(ctx, appcontext.FromGinContext(ctx), id, body), method) } // ImportVoucherCodes is POST /marketing/vouchers/:id/codes, with the CSV as // a multipart file named "file" or as the request body. func (h *EnakGameAdminHandler) ImportVoucherCodes(c *gin.Context) { const method = "EnakGameAdminHandler::ImportVoucherCodes" id, ok := pathID(c, "id", method) if !ok { return } var data []byte if strings.HasPrefix(c.ContentType(), "multipart/") { file, err := c.FormFile("file") if err == nil { var f multipart.File if f, err = file.Open(); err == nil { data, err = io.ReadAll(io.LimitReader(f, voucherCodeUploadLimit)) f.Close() } } if err != nil { util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "send the CSV as a file named \"file\""), }), method) return } } else if data, ok = rawBody(c, method); !ok { return } ctx := c.Request.Context() util.HandleResponse(c.Writer, c.Request, h.service.ImportVoucherCodes(ctx, appcontext.FromGinContext(ctx), id, data), method) } // ListVoucherCodes is GET /marketing/vouchers/:id/codes?status=&page=&limit=. func (h *EnakGameAdminHandler) ListVoucherCodes(c *gin.Context) { const method = "EnakGameAdminHandler::ListVoucherCodes" id, ok := pathID(c, "id", method) if !ok { return } var q models.VoucherCodeListQuery if !bindQuery(c, &q, method) { return } ctx := c.Request.Context() util.HandleResponse(c.Writer, c.Request, h.service.ListVoucherCodes(ctx, appcontext.FromGinContext(ctx), id, q), method) } // CreateEvent is POST /marketing/enakgame/events. func (h *EnakGameAdminHandler) CreateEvent(c *gin.Context) { const method = "EnakGameAdminHandler::CreateEvent" body, ok := rawBody(c, method) if !ok { return } ctx := c.Request.Context() util.HandleResponse(c.Writer, c.Request, h.service.CreateEvent(ctx, appcontext.FromGinContext(ctx), body), method) } // ListEvents is GET /marketing/enakgame/events?status=&page=&limit=. func (h *EnakGameAdminHandler) ListEvents(c *gin.Context) { const method = "EnakGameAdminHandler::ListEvents" var q models.GameEventListQuery if !bindQuery(c, &q, method) { return } ctx := c.Request.Context() util.HandleResponse(c.Writer, c.Request, h.service.ListEvents(ctx, appcontext.FromGinContext(ctx), q), method) } // GetEvent is GET /marketing/enakgame/events/:id. func (h *EnakGameAdminHandler) GetEvent(c *gin.Context) { const method = "EnakGameAdminHandler::GetEvent" id, ok := pathID(c, "id", method) if !ok { return } ctx := c.Request.Context() util.HandleResponse(c.Writer, c.Request, h.service.GetEvent(ctx, appcontext.FromGinContext(ctx), id), method) } // UpdateEvent is PUT /marketing/enakgame/events/:id. func (h *EnakGameAdminHandler) UpdateEvent(c *gin.Context) { const method = "EnakGameAdminHandler::UpdateEvent" id, ok := pathID(c, "id", method) if !ok { return } body, ok := rawBody(c, method) if !ok { return } ctx := c.Request.Context() util.HandleResponse(c.Writer, c.Request, h.service.UpdateEvent(ctx, appcontext.FromGinContext(ctx), id, body), method) } // SetEventStatus is PUT /marketing/enakgame/events/:id/status. func (h *EnakGameAdminHandler) SetEventStatus(c *gin.Context) { const method = "EnakGameAdminHandler::SetEventStatus" id, ok := pathID(c, "id", method) if !ok { return } body, ok := rawBody(c, method) if !ok { return } ctx := c.Request.Context() util.HandleResponse(c.Writer, c.Request, h.service.SetEventStatus(ctx, appcontext.FromGinContext(ctx), id, body), method) }