Vouchers are what EnakPoint is redeemed for, wherever it came from, so they are not part of EnakGame. Their only link to it is the budget attribution, which does not change. - Admin: /marketing/enakgame/vouchers... -> /marketing/vouchers... - Customer: /customer/enakgame/vouchers -> /customer/vouchers, /customer/enakgame/vouchers/:id/redeem -> /customer/vouchers/:id/redeem, /customer/enakgame/redemptions -> /customer/vouchers/redemptions Roles, handlers and logic stay the same. No client calls these endpoints yet. RFC §7.4 and §11 updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
459 lines
15 KiB
Go
459 lines
15 KiB
Go
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)
|
|
}
|