EnakGame phases 1-8 of docs/tasks-enakgame.md (EG-101 to EG-803), built on the existing EnakPoint/EnakCoin wallet (docs/rfc-enakgame.md). Foundation (phase 1) - Migrations 000103-000106: games extended with organization, slug, status, entry cost and result rules, old games archived (not deleted); budgets, versioned reward configs, sessions and session rewards; the ledger types GAME_SPEND_REFUND, GAME_REWARD and REWARD_REDEEM_REFUND; audit_logs. - AuditLogger writes in the caller's transaction only. - enakgame.limit.user_daily and global_daily organization settings. Games and sessions (phases 2-4) - Admin /marketing/enakgame: games, reward config versions (immutable but for status, one ACTIVE per game), budgets with non-overlapping global periods and a daily job opening the next month. - Customer /customer/enakgame: start (Idempotency-Key, entry cost and config frozen on the session), complete (result validation, reward engine, max_reward cap, daily limits via game_reward_counters, one GAME_REWARD per budget), automatic refunds for system errors and deactivated games, and a session job. - Reward engine: FIXED, SCORE_BASED, OUTCOME_BASED, PROBABILITY (crypto/rand), rounded down. Vouchers and budgets (phases 5-6) - Migration 000108 and 000107: vouchers, codes, redemptions, cost attribution; Economy Guard counters. - STATIC and CODE_POOL redemption in one transaction with the REDEEM PIN action; realized cost traced through the lots to the budget that paid the reward. - Budget metrics: realized cost, forecast, exposure and status. Migrations 000109-000110 add the wallet_lots indexes they need, built CONCURRENTLY. Events (phase 7) - Migration 000111: game events, each with its own EVENT budget. Event extras stack per PRD §16 defaults, with event and per-customer limits. External vouchers (phase 8) - VoucherProvider contract, two-step PENDING redemption and a recovery job, tested with a fake provider. No provider adapter is registered yet, so EXTERNAL vouchers stay out of the catalog. Not yet decided before release: reward rounding, event stacking, budget exhaustion policy and thresholds (RFC §19.2). Migrations 000103-000111 have not been run on any shared database. Also fixes a leftover PAYMENT filter in a wallet test and a data race in a test PIN fake. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
396 lines
18 KiB
Go
396 lines
18 KiB
Go
package service
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"apskel-pos-be/internal/appcontext"
|
|
"apskel-pos-be/internal/constants"
|
|
"apskel-pos-be/internal/contract"
|
|
"apskel-pos-be/internal/logger"
|
|
"apskel-pos-be/internal/models"
|
|
"apskel-pos-be/internal/processor"
|
|
"apskel-pos-be/internal/repository"
|
|
)
|
|
|
|
// EnakGameAdminService is the dashboard side of EnakGame: games, reward
|
|
// configurations and budgets of the admin's organization (docs/rfc-enakgame.md §11).
|
|
// Bodies are read strictly, so a misspelt field is refused instead of ignored.
|
|
type EnakGameAdminService interface {
|
|
CreateGame(ctx context.Context, apctx *appcontext.ContextInfo, body []byte) *contract.Response
|
|
ListGames(ctx context.Context, apctx *appcontext.ContextInfo, q models.EnakGameListQuery) *contract.Response
|
|
GetGame(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID) *contract.Response
|
|
// UpdateGame changes the fields the body sends and keeps the others.
|
|
UpdateGame(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID, body []byte) *contract.Response
|
|
SetGameStatus(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID, body []byte) *contract.Response
|
|
|
|
CreateRewardConfig(ctx context.Context, apctx *appcontext.ContextInfo, gameID uuid.UUID, body []byte) *contract.Response
|
|
ListRewardConfigs(ctx context.Context, apctx *appcontext.ContextInfo, gameID uuid.UUID) *contract.Response
|
|
ActivateRewardConfig(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID, body []byte) *contract.Response
|
|
|
|
CreateBudget(ctx context.Context, apctx *appcontext.ContextInfo, body []byte) *contract.Response
|
|
ListBudgets(ctx context.Context, apctx *appcontext.ContextInfo, q models.GameBudgetListQuery) *contract.Response
|
|
GetBudget(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID) *contract.Response
|
|
// UpdateBudget changes the fields the body sends and keeps the others.
|
|
UpdateBudget(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID, body []byte) *contract.Response
|
|
DeleteBudget(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID) *contract.Response
|
|
BudgetMetrics(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID) *contract.Response
|
|
|
|
CreateEvent(ctx context.Context, apctx *appcontext.ContextInfo, body []byte) *contract.Response
|
|
ListEvents(ctx context.Context, apctx *appcontext.ContextInfo, q models.GameEventListQuery) *contract.Response
|
|
GetEvent(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID) *contract.Response
|
|
// UpdateEvent changes the fields the body sends and keeps the others.
|
|
UpdateEvent(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID, body []byte) *contract.Response
|
|
SetEventStatus(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID, body []byte) *contract.Response
|
|
|
|
CreateVoucher(ctx context.Context, apctx *appcontext.ContextInfo, body []byte) *contract.Response
|
|
ListVouchers(ctx context.Context, apctx *appcontext.ContextInfo, q models.VoucherListQuery) *contract.Response
|
|
GetVoucher(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID) *contract.Response
|
|
// UpdateVoucher changes the fields the body sends and keeps the others.
|
|
UpdateVoucher(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID, body []byte) *contract.Response
|
|
SetVoucherStatus(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID, body []byte) *contract.Response
|
|
// ImportVoucherCodes adds the codes of a CSV to a CODE_POOL voucher.
|
|
ImportVoucherCodes(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID, csv []byte) *contract.Response
|
|
ListVoucherCodes(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID, q models.VoucherCodeListQuery) *contract.Response
|
|
}
|
|
|
|
// EnakGameCustomerService is what the customer app does with EnakGame.
|
|
type EnakGameCustomerService interface {
|
|
ListGames(ctx context.Context, customerID uuid.UUID) *contract.Response
|
|
StartSession(ctx context.Context, customerID uuid.UUID, req *contract.StartGameSessionRequest, idempotencyKey string) *contract.Response
|
|
CompleteSession(ctx context.Context, customerID, sessionID uuid.UUID, in models.GameSessionCompleteInput) *contract.Response
|
|
ListSessions(ctx context.Context, customerID uuid.UUID, page, limit int) *contract.Response
|
|
GetSession(ctx context.Context, customerID, sessionID uuid.UUID) *contract.Response
|
|
|
|
ListVouchers(ctx context.Context, customerID uuid.UUID) *contract.Response
|
|
RedeemVoucher(ctx context.Context, customerID, voucherID uuid.UUID, req *contract.RedeemVoucherRequest, idempotencyKey string, info models.CustomerPinRequestInfo) *contract.Response
|
|
ListRedemptions(ctx context.Context, customerID uuid.UUID, page, limit int) *contract.Response
|
|
}
|
|
|
|
type EnakGameAdminServiceImpl struct {
|
|
games *processor.EnakGameAdminProcessor
|
|
budgets *processor.GameBudgetProcessor
|
|
metrics *processor.GameBudgetMetricsProcessor
|
|
events *processor.GameEventProcessor
|
|
vouchers *processor.VoucherAdminProcessor
|
|
}
|
|
|
|
func NewEnakGameAdminService(games *processor.EnakGameAdminProcessor, budgets *processor.GameBudgetProcessor, metrics *processor.GameBudgetMetricsProcessor,
|
|
events *processor.GameEventProcessor, vouchers *processor.VoucherAdminProcessor) *EnakGameAdminServiceImpl {
|
|
return &EnakGameAdminServiceImpl{games: games, budgets: budgets, metrics: metrics, events: events, vouchers: vouchers}
|
|
}
|
|
|
|
type EnakGameCustomerServiceImpl struct {
|
|
sessions *processor.GameSessionProcessor
|
|
redemptions *processor.VoucherRedemptionProcessor
|
|
}
|
|
|
|
func NewEnakGameCustomerService(sessions *processor.GameSessionProcessor, redemptions *processor.VoucherRedemptionProcessor) *EnakGameCustomerServiceImpl {
|
|
return &EnakGameCustomerServiceImpl{sessions: sessions, redemptions: redemptions}
|
|
}
|
|
|
|
// respond wraps a processor result, or its error, as a response.
|
|
func respond[T any](ctx context.Context, result T, err error) *contract.Response {
|
|
if err != nil {
|
|
return enakGameErrorResponse(ctx, err)
|
|
}
|
|
return contract.BuildSuccessResponse(result)
|
|
}
|
|
|
|
func (s *EnakGameAdminServiceImpl) CreateGame(ctx context.Context, apctx *appcontext.ContextInfo, body []byte) *contract.Response {
|
|
var in models.EnakGameInput
|
|
if resp := decodeStrict(body, &in); resp != nil {
|
|
return resp
|
|
}
|
|
game, err := s.games.CreateGame(ctx, apctx.OrganizationID, apctx.UserID, in)
|
|
return respond(ctx, game, err)
|
|
}
|
|
|
|
func (s *EnakGameAdminServiceImpl) ListGames(ctx context.Context, apctx *appcontext.ContextInfo, q models.EnakGameListQuery) *contract.Response {
|
|
games, err := s.games.ListGames(ctx, apctx.OrganizationID, q)
|
|
return respond(ctx, games, err)
|
|
}
|
|
|
|
func (s *EnakGameAdminServiceImpl) GetGame(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID) *contract.Response {
|
|
game, err := s.games.GetGame(ctx, apctx.OrganizationID, id)
|
|
return respond(ctx, game, err)
|
|
}
|
|
|
|
func (s *EnakGameAdminServiceImpl) UpdateGame(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID, body []byte) *contract.Response {
|
|
current, err := s.games.GetGame(ctx, apctx.OrganizationID, id)
|
|
if err != nil {
|
|
return enakGameErrorResponse(ctx, err)
|
|
}
|
|
in := processor.EnakGameInputFrom(current)
|
|
if resp := decodeStrict(body, &in); resp != nil {
|
|
return resp
|
|
}
|
|
game, err := s.games.UpdateGame(ctx, apctx.OrganizationID, apctx.UserID, id, in)
|
|
return respond(ctx, game, err)
|
|
}
|
|
|
|
func (s *EnakGameAdminServiceImpl) SetGameStatus(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID, body []byte) *contract.Response {
|
|
var in models.EnakGameStatusInput
|
|
if resp := decodeStrict(body, &in); resp != nil {
|
|
return resp
|
|
}
|
|
game, err := s.games.SetGameStatus(ctx, apctx.OrganizationID, apctx.UserID, id, in)
|
|
return respond(ctx, game, err)
|
|
}
|
|
|
|
func (s *EnakGameAdminServiceImpl) CreateRewardConfig(ctx context.Context, apctx *appcontext.ContextInfo, gameID uuid.UUID, body []byte) *contract.Response {
|
|
var in models.GameRewardConfigInput
|
|
if resp := decodeStrict(body, &in); resp != nil {
|
|
return resp
|
|
}
|
|
config, err := s.games.CreateRewardConfig(ctx, apctx.OrganizationID, apctx.UserID, gameID, in)
|
|
return respond(ctx, config, err)
|
|
}
|
|
|
|
func (s *EnakGameAdminServiceImpl) ListRewardConfigs(ctx context.Context, apctx *appcontext.ContextInfo, gameID uuid.UUID) *contract.Response {
|
|
configs, err := s.games.ListRewardConfigs(ctx, apctx.OrganizationID, gameID)
|
|
return respond(ctx, configs, err)
|
|
}
|
|
|
|
func (s *EnakGameAdminServiceImpl) ActivateRewardConfig(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID, body []byte) *contract.Response {
|
|
var in models.GameRewardConfigActivateInput
|
|
if len(bytes.TrimSpace(body)) > 0 {
|
|
if resp := decodeStrict(body, &in); resp != nil {
|
|
return resp
|
|
}
|
|
}
|
|
config, err := s.games.ActivateRewardConfig(ctx, apctx.OrganizationID, apctx.UserID, id, in)
|
|
return respond(ctx, config, err)
|
|
}
|
|
|
|
func (s *EnakGameAdminServiceImpl) CreateBudget(ctx context.Context, apctx *appcontext.ContextInfo, body []byte) *contract.Response {
|
|
var in models.GameBudgetInput
|
|
if resp := decodeStrict(body, &in); resp != nil {
|
|
return resp
|
|
}
|
|
budget, err := s.budgets.CreateBudget(ctx, apctx.OrganizationID, apctx.UserID, in)
|
|
return respond(ctx, budget, err)
|
|
}
|
|
|
|
func (s *EnakGameAdminServiceImpl) ListBudgets(ctx context.Context, apctx *appcontext.ContextInfo, q models.GameBudgetListQuery) *contract.Response {
|
|
budgets, err := s.budgets.ListBudgets(ctx, apctx.OrganizationID, q)
|
|
return respond(ctx, budgets, err)
|
|
}
|
|
|
|
func (s *EnakGameAdminServiceImpl) GetBudget(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID) *contract.Response {
|
|
budget, err := s.budgets.GetBudget(ctx, apctx.OrganizationID, id)
|
|
return respond(ctx, budget, err)
|
|
}
|
|
|
|
func (s *EnakGameAdminServiceImpl) UpdateBudget(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID, body []byte) *contract.Response {
|
|
current, err := s.budgets.GetBudget(ctx, apctx.OrganizationID, id)
|
|
if err != nil {
|
|
return enakGameErrorResponse(ctx, err)
|
|
}
|
|
in := processor.GameBudgetInputFrom(current)
|
|
if resp := decodeStrict(body, &in); resp != nil {
|
|
return resp
|
|
}
|
|
budget, err := s.budgets.UpdateBudget(ctx, apctx.OrganizationID, apctx.UserID, id, in)
|
|
return respond(ctx, budget, err)
|
|
}
|
|
|
|
func (s *EnakGameAdminServiceImpl) DeleteBudget(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID) *contract.Response {
|
|
if err := s.budgets.DeleteBudget(ctx, apctx.OrganizationID, apctx.UserID, id); err != nil {
|
|
return enakGameErrorResponse(ctx, err)
|
|
}
|
|
return contract.BuildSuccessResponse(map[string]any{"id": id, "deleted": true})
|
|
}
|
|
|
|
func (s *EnakGameAdminServiceImpl) BudgetMetrics(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID) *contract.Response {
|
|
metrics, err := s.metrics.Metrics(ctx, apctx.OrganizationID, id)
|
|
return respond(ctx, metrics, err)
|
|
}
|
|
|
|
func (s *EnakGameAdminServiceImpl) CreateEvent(ctx context.Context, apctx *appcontext.ContextInfo, body []byte) *contract.Response {
|
|
var in models.GameEventInput
|
|
if resp := decodeStrict(body, &in); resp != nil {
|
|
return resp
|
|
}
|
|
event, err := s.events.CreateEvent(ctx, apctx.OrganizationID, apctx.UserID, in)
|
|
return respond(ctx, event, err)
|
|
}
|
|
|
|
func (s *EnakGameAdminServiceImpl) ListEvents(ctx context.Context, apctx *appcontext.ContextInfo, q models.GameEventListQuery) *contract.Response {
|
|
events, err := s.events.ListEvents(ctx, apctx.OrganizationID, q)
|
|
return respond(ctx, events, err)
|
|
}
|
|
|
|
func (s *EnakGameAdminServiceImpl) GetEvent(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID) *contract.Response {
|
|
event, err := s.events.GetEvent(ctx, apctx.OrganizationID, id)
|
|
return respond(ctx, event, err)
|
|
}
|
|
|
|
func (s *EnakGameAdminServiceImpl) UpdateEvent(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID, body []byte) *contract.Response {
|
|
current, err := s.events.GetEvent(ctx, apctx.OrganizationID, id)
|
|
if err != nil {
|
|
return enakGameErrorResponse(ctx, err)
|
|
}
|
|
in := processor.GameEventInputFrom(current)
|
|
if resp := decodeStrict(body, &in); resp != nil {
|
|
return resp
|
|
}
|
|
event, err := s.events.UpdateEvent(ctx, apctx.OrganizationID, apctx.UserID, id, in)
|
|
return respond(ctx, event, err)
|
|
}
|
|
|
|
func (s *EnakGameAdminServiceImpl) SetEventStatus(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID, body []byte) *contract.Response {
|
|
var in models.GameEventStatusInput
|
|
if resp := decodeStrict(body, &in); resp != nil {
|
|
return resp
|
|
}
|
|
event, err := s.events.SetEventStatus(ctx, apctx.OrganizationID, apctx.UserID, id, in)
|
|
return respond(ctx, event, err)
|
|
}
|
|
|
|
func (s *EnakGameAdminServiceImpl) CreateVoucher(ctx context.Context, apctx *appcontext.ContextInfo, body []byte) *contract.Response {
|
|
var in models.VoucherInput
|
|
if resp := decodeStrict(body, &in); resp != nil {
|
|
return resp
|
|
}
|
|
voucher, err := s.vouchers.CreateVoucher(ctx, apctx.OrganizationID, apctx.UserID, in)
|
|
return respond(ctx, voucher, err)
|
|
}
|
|
|
|
func (s *EnakGameAdminServiceImpl) ListVouchers(ctx context.Context, apctx *appcontext.ContextInfo, q models.VoucherListQuery) *contract.Response {
|
|
vouchers, err := s.vouchers.ListVouchers(ctx, apctx.OrganizationID, q)
|
|
return respond(ctx, vouchers, err)
|
|
}
|
|
|
|
func (s *EnakGameAdminServiceImpl) GetVoucher(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID) *contract.Response {
|
|
voucher, err := s.vouchers.GetVoucher(ctx, apctx.OrganizationID, id)
|
|
return respond(ctx, voucher, err)
|
|
}
|
|
|
|
func (s *EnakGameAdminServiceImpl) UpdateVoucher(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID, body []byte) *contract.Response {
|
|
current, err := s.vouchers.GetVoucher(ctx, apctx.OrganizationID, id)
|
|
if err != nil {
|
|
return enakGameErrorResponse(ctx, err)
|
|
}
|
|
in := processor.VoucherInputFrom(current)
|
|
if resp := decodeStrict(body, &in); resp != nil {
|
|
return resp
|
|
}
|
|
voucher, err := s.vouchers.UpdateVoucher(ctx, apctx.OrganizationID, apctx.UserID, id, in)
|
|
return respond(ctx, voucher, err)
|
|
}
|
|
|
|
func (s *EnakGameAdminServiceImpl) SetVoucherStatus(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID, body []byte) *contract.Response {
|
|
var in models.VoucherStatusInput
|
|
if resp := decodeStrict(body, &in); resp != nil {
|
|
return resp
|
|
}
|
|
voucher, err := s.vouchers.SetVoucherStatus(ctx, apctx.OrganizationID, apctx.UserID, id, in)
|
|
return respond(ctx, voucher, err)
|
|
}
|
|
|
|
func (s *EnakGameAdminServiceImpl) ImportVoucherCodes(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID, csv []byte) *contract.Response {
|
|
result, err := s.vouchers.ImportCodes(ctx, apctx.OrganizationID, apctx.UserID, id, csv)
|
|
return respond(ctx, result, err)
|
|
}
|
|
|
|
func (s *EnakGameAdminServiceImpl) ListVoucherCodes(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID, q models.VoucherCodeListQuery) *contract.Response {
|
|
codes, err := s.vouchers.Codes(ctx, apctx.OrganizationID, id, q)
|
|
return respond(ctx, codes, err)
|
|
}
|
|
|
|
func (s *EnakGameCustomerServiceImpl) ListVouchers(ctx context.Context, customerID uuid.UUID) *contract.Response {
|
|
vouchers, err := s.redemptions.Catalog(ctx, customerID)
|
|
return respond(ctx, vouchers, err)
|
|
}
|
|
|
|
func (s *EnakGameCustomerServiceImpl) RedeemVoucher(ctx context.Context, customerID, voucherID uuid.UUID, req *contract.RedeemVoucherRequest, idempotencyKey string, info models.CustomerPinRequestInfo) *contract.Response {
|
|
result, err := s.redemptions.Redeem(ctx, customerID, voucherID, req.Pin, idempotencyKey, info)
|
|
return respond(ctx, result, err)
|
|
}
|
|
|
|
func (s *EnakGameCustomerServiceImpl) ListRedemptions(ctx context.Context, customerID uuid.UUID, page, limit int) *contract.Response {
|
|
redemptions, err := s.redemptions.ListRedemptions(ctx, customerID, page, limit)
|
|
return respond(ctx, redemptions, err)
|
|
}
|
|
|
|
func (s *EnakGameCustomerServiceImpl) ListGames(ctx context.Context, customerID uuid.UUID) *contract.Response {
|
|
games, err := s.sessions.ListGames(ctx, customerID)
|
|
return respond(ctx, games, err)
|
|
}
|
|
|
|
func (s *EnakGameCustomerServiceImpl) StartSession(ctx context.Context, customerID uuid.UUID, req *contract.StartGameSessionRequest, idempotencyKey string) *contract.Response {
|
|
started, err := s.sessions.Start(ctx, customerID, req.GameID, idempotencyKey)
|
|
return respond(ctx, started, err)
|
|
}
|
|
|
|
func (s *EnakGameCustomerServiceImpl) CompleteSession(ctx context.Context, customerID, sessionID uuid.UUID, in models.GameSessionCompleteInput) *contract.Response {
|
|
completion, err := s.sessions.Complete(ctx, customerID, sessionID, in)
|
|
return respond(ctx, completion, err)
|
|
}
|
|
|
|
func (s *EnakGameCustomerServiceImpl) ListSessions(ctx context.Context, customerID uuid.UUID, page, limit int) *contract.Response {
|
|
sessions, err := s.sessions.ListSessions(ctx, customerID, page, limit)
|
|
return respond(ctx, sessions, err)
|
|
}
|
|
|
|
func (s *EnakGameCustomerServiceImpl) GetSession(ctx context.Context, customerID, sessionID uuid.UUID) *contract.Response {
|
|
session, err := s.sessions.GetSession(ctx, customerID, sessionID)
|
|
return respond(ctx, session, err)
|
|
}
|
|
|
|
// decodeStrict reads a JSON body into v, refusing unknown fields and trailing data.
|
|
// It returns the error response, or nil when the body fits.
|
|
func decodeStrict(body []byte, v any) *contract.Response {
|
|
dec := json.NewDecoder(bytes.NewReader(body))
|
|
dec.DisallowUnknownFields()
|
|
err := dec.Decode(v)
|
|
if err == nil {
|
|
if _, trailing := dec.Token(); trailing != io.EOF {
|
|
err = errors.New("unexpected data after the JSON object")
|
|
}
|
|
}
|
|
if err != nil {
|
|
return contract.BuildErrorResponse([]*contract.ResponseError{
|
|
contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, fmt.Sprintf("invalid request body: %v", err)),
|
|
})
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// enakGameErrorResponse tells a refused or missing thing apart from a server failure,
|
|
// whose details stay in the log.
|
|
func enakGameErrorResponse(ctx context.Context, err error) *contract.Response {
|
|
var pinErr *processor.PinError
|
|
if errors.As(err, &pinErr) {
|
|
return PinErrorResponse(err)
|
|
}
|
|
code, cause := constants.InternalServerErrorCode, "internal error"
|
|
switch {
|
|
case errors.Is(err, repository.ErrEnakGameNotFound),
|
|
errors.Is(err, repository.ErrGameRewardConfigNotFound),
|
|
errors.Is(err, repository.ErrGameBudgetNotFound),
|
|
errors.Is(err, repository.ErrGameSessionNotFound),
|
|
errors.Is(err, repository.ErrVoucherNotFound),
|
|
errors.Is(err, repository.ErrGameEventNotFound),
|
|
errors.Is(err, repository.ErrWalletNotFound):
|
|
code, cause = constants.NotFoundErrorCode, err.Error()
|
|
case errors.Is(err, processor.ErrEnakGameRejected),
|
|
errors.Is(err, processor.ErrGameSessionRejected),
|
|
errors.Is(err, processor.ErrVoucherRedemptionRejected),
|
|
errors.Is(err, processor.ErrWalletIdempotencyConflict):
|
|
code, cause = constants.ValidationErrorCode, err.Error()
|
|
default:
|
|
logger.FromContext(ctx).WithError(err).Error("EnakGame request failed")
|
|
}
|
|
return contract.BuildErrorResponse([]*contract.ResponseError{
|
|
contract.NewResponseError(code, constants.EnakGameServiceEntity, cause),
|
|
})
|
|
}
|