Files
apskel-pos-backend/internal/handler/enakgame_customer_handler.go
T
efrilmandClaude Opus 5.5 1bb071aec6 refactor(vouchers): move voucher routes out of /enakgame
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>
2026-10-08 11:09:57 +07:00

149 lines
5.0 KiB
Go

package handler
import (
"bytes"
"encoding/json"
"strconv"
"github.com/gin-gonic/gin"
"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"
)
// EnakGameCustomerHandler serves /customer/enakgame (docs/rfc-enakgame.md §11).
type EnakGameCustomerHandler struct {
service service.EnakGameCustomerService
}
func NewEnakGameCustomerHandler(s service.EnakGameCustomerService) *EnakGameCustomerHandler {
return &EnakGameCustomerHandler{service: s}
}
// ListGames is GET /customer/enakgame/games.
func (h *EnakGameCustomerHandler) ListGames(c *gin.Context) {
const method = "EnakGameCustomerHandler::ListGames"
customerID, ok := customerIDFromGin(c, method)
if !ok {
return
}
util.HandleResponse(c.Writer, c.Request, h.service.ListGames(c.Request.Context(), customerID), method)
}
// StartSession is POST /customer/enakgame/sessions. It requires Idempotency-Key.
func (h *EnakGameCustomerHandler) StartSession(c *gin.Context) {
const method = "EnakGameCustomerHandler::StartSession"
customerID, ok := customerIDFromGin(c, method)
if !ok {
return
}
var req contract.StartGameSessionRequest
if err := c.ShouldBindJSON(&req); err != nil {
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, "game_id is required"),
}), method)
return
}
util.HandleResponse(c.Writer, c.Request, h.service.StartSession(c.Request.Context(), customerID, &req, idempotencyKey(c)), method)
}
// ListSessions is GET /customer/enakgame/sessions?page=&limit=.
func (h *EnakGameCustomerHandler) ListSessions(c *gin.Context) {
const method = "EnakGameCustomerHandler::ListSessions"
customerID, ok := customerIDFromGin(c, method)
if !ok {
return
}
page, _ := strconv.Atoi(c.Query("page"))
limit, _ := strconv.Atoi(c.Query("limit"))
util.HandleResponse(c.Writer, c.Request, h.service.ListSessions(c.Request.Context(), customerID, page, limit), method)
}
// GetSession is GET /customer/enakgame/sessions/:id.
func (h *EnakGameCustomerHandler) GetSession(c *gin.Context) {
const method = "EnakGameCustomerHandler::GetSession"
customerID, ok := customerIDFromGin(c, method)
if !ok {
return
}
id, ok := pathID(c, "id", method)
if !ok {
return
}
util.HandleResponse(c.Writer, c.Request, h.service.GetSession(c.Request.Context(), customerID, id), method)
}
// CompleteSession is POST /customer/enakgame/sessions/:id/complete. The body is read
// leniently on purpose: a reward amount, or anything else the backend does not take
// from the client, is ignored rather than refused (P1). An empty body is a result
// with nothing in it.
func (h *EnakGameCustomerHandler) CompleteSession(c *gin.Context) {
const method = "EnakGameCustomerHandler::CompleteSession"
customerID, ok := customerIDFromGin(c, method)
if !ok {
return
}
id, ok := pathID(c, "id", method)
if !ok {
return
}
body, ok := rawBody(c, method)
if !ok {
return
}
var in models.GameSessionCompleteInput
if len(bytes.TrimSpace(body)) > 0 {
if err := json.Unmarshal(body, &in); err != nil {
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "score must be a whole number and outcome a text"),
}), method)
return
}
}
util.HandleResponse(c.Writer, c.Request, h.service.CompleteSession(c.Request.Context(), customerID, id, in), method)
}
// ListVouchers is GET /customer/vouchers.
func (h *EnakGameCustomerHandler) ListVouchers(c *gin.Context) {
const method = "EnakGameCustomerHandler::ListVouchers"
customerID, ok := customerIDFromGin(c, method)
if !ok {
return
}
util.HandleResponse(c.Writer, c.Request, h.service.ListVouchers(c.Request.Context(), customerID), method)
}
// RedeemVoucher is POST /customer/vouchers/:id/redeem. It requires the PIN
// and Idempotency-Key; the body holds the PIN, so it is never logged.
func (h *EnakGameCustomerHandler) RedeemVoucher(c *gin.Context) {
const method = "EnakGameCustomerHandler::RedeemVoucher"
customerID, ok := customerIDFromGin(c, method)
if !ok {
return
}
id, ok := pathID(c, "id", method)
if !ok {
return
}
var req contract.RedeemVoucherRequest
if !bindPinRequest(c, &req, method) {
return
}
util.HandleResponse(c.Writer, c.Request, h.service.RedeemVoucher(c.Request.Context(), customerID, id, &req, idempotencyKey(c), pinRequestInfo(c)), method)
}
// ListRedemptions is GET /customer/vouchers/redemptions?page=&limit=.
func (h *EnakGameCustomerHandler) ListRedemptions(c *gin.Context) {
const method = "EnakGameCustomerHandler::ListRedemptions"
customerID, ok := customerIDFromGin(c, method)
if !ok {
return
}
page, _ := strconv.Atoi(c.Query("page"))
limit, _ := strconv.Atoi(c.Query("limit"))
util.HandleResponse(c.Writer, c.Request, h.service.ListRedemptions(c.Request.Context(), customerID, page, limit), method)
}