Files
apskel-pos-backend/internal/transformer/gamification_transformer.go
T
efrilmandClaude Opus 5.5 a18bb072f5 feat(loyalty): pay every game with EnakCoin
Games now spend the wallet's EnakCoin instead of the per-type tokens
(docs/prd-point-coin.md F8, K1, PC-403).

GamePlayProcessor.PlayGame charges the game's metadata.coin_cost, 1 when
it is not set; a cost that is not a whole number of at least 1 refuses the
game. In one transaction it picks the prize, takes the EnakCoin with a
GAME_SPEND row pointing at the new game_plays.id (which locks the wallet,
so a customer's plays at the same time queue up), records the play and
takes the prize from stock. The play owns its transaction, so the spin
service no longer wraps it, and the admin play endpoint is now atomic too.

The game, game prize and game play repositories go through DBFromContext
so they join that transaction. DecreaseStock now reports a prize that ran
out (ErrGamePrizeOutOfStock) instead of silently updating nothing; that,
or any other stock failure, cancels the whole play, where it used to be
only printed. The manual AddTokens rollback is gone. Not enough EnakCoin,
an inactive game or a prize that ran out answer 400 on /customer/spin
instead of 500.

game_plays.token_used is renamed coins_used (migration 000095). What a
play costs is no longer the caller's choice, so PlayGameRequest loses
token_used. Responses carry coins_used and coins_remaining; token_used and
tokens_remaining stay as deprecated copies until the apps move over, and
sort_by=token_used still sorts by coins_used.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 12:20:42 +07:00

528 lines
14 KiB
Go

package transformer
import (
"apskel-pos-be/internal/contract"
"apskel-pos-be/internal/models"
)
// Customer Points Transformers
func CreateCustomerPointsRequestToModel(req *contract.CreateCustomerPointsRequest) *models.CreateCustomerPointsRequest {
return &models.CreateCustomerPointsRequest{
CustomerID: req.CustomerID,
Balance: req.Balance,
}
}
func UpdateCustomerPointsRequestToModel(req *contract.UpdateCustomerPointsRequest) *models.UpdateCustomerPointsRequest {
return &models.UpdateCustomerPointsRequest{
Balance: req.Balance,
}
}
func ListCustomerPointsRequestToModel(req *contract.ListCustomerPointsRequest) *models.ListCustomerPointsQuery {
return &models.ListCustomerPointsQuery{
Page: req.Page,
Limit: req.Limit,
Search: req.Search,
SortBy: req.SortBy,
SortOrder: req.SortOrder,
}
}
func CustomerPointsModelToResponse(model *models.CustomerPointsResponse) *contract.CustomerPointsResponse {
if model == nil {
return nil
}
var customer *contract.CustomerResponse
if model.Customer != nil {
customer = CustomerModelToResponse(model.Customer)
}
return &contract.CustomerPointsResponse{
ID: model.ID,
CustomerID: model.CustomerID,
Balance: model.Balance,
Customer: customer,
CreatedAt: model.CreatedAt,
UpdatedAt: model.UpdatedAt,
}
}
func PaginatedCustomerPointsResponseToContract(model *models.PaginatedCustomerPointsResponse) *contract.PaginatedCustomerPointsResponse {
if model == nil {
return nil
}
responses := make([]contract.CustomerPointsResponse, len(model.Data))
for i, item := range model.Data {
responses[i] = *CustomerPointsModelToResponse(&item)
}
return &contract.PaginatedCustomerPointsResponse{
Data: responses,
TotalCount: model.TotalCount,
Page: model.Page,
Limit: model.Limit,
TotalPages: model.TotalPages,
}
}
// Customer Tokens Transformers
func CreateCustomerTokensRequestToModel(req *contract.CreateCustomerTokensRequest) *models.CreateCustomerTokensRequest {
return &models.CreateCustomerTokensRequest{
CustomerID: req.CustomerID,
TokenType: req.TokenType,
Balance: req.Balance,
}
}
func UpdateCustomerTokensRequestToModel(req *contract.UpdateCustomerTokensRequest) *models.UpdateCustomerTokensRequest {
return &models.UpdateCustomerTokensRequest{
Balance: req.Balance,
}
}
func ListCustomerTokensRequestToModel(req *contract.ListCustomerTokensRequest) *models.ListCustomerTokensQuery {
return &models.ListCustomerTokensQuery{
Page: req.Page,
Limit: req.Limit,
Search: req.Search,
TokenType: req.TokenType,
SortBy: req.SortBy,
SortOrder: req.SortOrder,
}
}
func CustomerTokensModelToResponse(model *models.CustomerTokensResponse) *contract.CustomerTokensResponse {
if model == nil {
return nil
}
var customer *contract.CustomerResponse
if model.Customer != nil {
customer = CustomerModelToResponse(model.Customer)
}
return &contract.CustomerTokensResponse{
ID: model.ID,
CustomerID: model.CustomerID,
TokenType: model.TokenType,
Balance: model.Balance,
Customer: customer,
CreatedAt: model.CreatedAt,
UpdatedAt: model.UpdatedAt,
}
}
func PaginatedCustomerTokensResponseToContract(model *models.PaginatedResponse[models.CustomerTokensResponse]) *contract.PaginatedCustomerTokensResponse {
responses := make([]contract.CustomerTokensResponse, len(model.Data))
for i, item := range model.Data {
responses[i] = *CustomerTokensModelToResponse(&item)
}
return &contract.PaginatedCustomerTokensResponse{
Data: responses,
TotalCount: int(model.Pagination.Total),
Page: model.Pagination.Page,
Limit: model.Pagination.Limit,
TotalPages: model.Pagination.TotalPages,
}
}
// Tier Transformers
func CreateTierRequestToModel(req *contract.CreateTierRequest) *models.CreateTierRequest {
return &models.CreateTierRequest{
Name: req.Name,
MinPoints: req.MinPoints,
Benefits: req.Benefits,
}
}
func UpdateTierRequestToModel(req *contract.UpdateTierRequest) *models.UpdateTierRequest {
return &models.UpdateTierRequest{
Name: req.Name,
MinPoints: req.MinPoints,
Benefits: req.Benefits,
}
}
func ListTiersRequestToModel(req *contract.ListTiersRequest) *models.ListTiersQuery {
return &models.ListTiersQuery{
Page: req.Page,
Limit: req.Limit,
Search: req.Search,
SortBy: req.SortBy,
SortOrder: req.SortOrder,
}
}
func TierModelToResponse(model *models.TierResponse) *contract.TierResponse {
return &contract.TierResponse{
ID: model.ID,
Name: model.Name,
MinPoints: model.MinPoints,
Benefits: model.Benefits,
CreatedAt: model.CreatedAt,
UpdatedAt: model.UpdatedAt,
}
}
func PaginatedTiersResponseToContract(model *models.PaginatedResponse[models.TierResponse]) *contract.PaginatedTiersResponse {
responses := make([]contract.TierResponse, len(model.Data))
for i, item := range model.Data {
responses[i] = *TierModelToResponse(&item)
}
return &contract.PaginatedTiersResponse{
Data: responses,
TotalCount: int(model.Pagination.Total),
Page: model.Pagination.Page,
Limit: model.Pagination.Limit,
TotalPages: model.Pagination.TotalPages,
}
}
// Game Transformers
func CreateGameRequestToModel(req *contract.CreateGameRequest) *models.CreateGameRequest {
return &models.CreateGameRequest{
Name: req.Name,
Type: req.Type,
IsActive: req.IsActive,
Metadata: req.Metadata,
}
}
func UpdateGameRequestToModel(req *contract.UpdateGameRequest) *models.UpdateGameRequest {
return &models.UpdateGameRequest{
Name: req.Name,
Type: req.Type,
IsActive: req.IsActive,
Metadata: req.Metadata,
}
}
func ListGamesRequestToModel(req *contract.ListGamesRequest) *models.ListGamesQuery {
return &models.ListGamesQuery{
Page: req.Page,
Limit: req.Limit,
Search: req.Search,
Type: req.Type,
IsActive: req.IsActive,
SortBy: req.SortBy,
SortOrder: req.SortOrder,
}
}
func GameModelToResponse(model *models.GameResponse) *contract.GameResponse {
if model == nil {
return nil
}
return &contract.GameResponse{
ID: model.ID,
Name: model.Name,
Type: model.Type,
IsActive: model.IsActive,
Metadata: model.Metadata,
CreatedAt: model.CreatedAt,
UpdatedAt: model.UpdatedAt,
}
}
func GameModelsToResponses(models []models.GameResponse) []contract.GameResponse {
responses := make([]contract.GameResponse, len(models))
for i, model := range models {
responses[i] = *GameModelToResponse(&model)
}
return responses
}
func PaginatedGamesResponseToContract(model *models.PaginatedResponse[models.GameResponse]) *contract.PaginatedGamesResponse {
responses := make([]contract.GameResponse, len(model.Data))
for i, item := range model.Data {
responses[i] = *GameModelToResponse(&item)
}
return &contract.PaginatedGamesResponse{
Data: responses,
TotalCount: int(model.Pagination.Total),
Page: model.Pagination.Page,
Limit: model.Pagination.Limit,
TotalPages: model.Pagination.TotalPages,
}
}
// Game Prize Transformers
func CreateGamePrizeRequestToModel(req *contract.CreateGamePrizeRequest) *models.CreateGamePrizeRequest {
return &models.CreateGamePrizeRequest{
GameID: req.GameID,
Name: req.Name,
Weight: req.Weight,
Stock: req.Stock,
MaxStock: req.MaxStock,
Threshold: req.Threshold,
FallbackPrizeID: req.FallbackPrizeID,
Metadata: req.Metadata,
}
}
func UpdateGamePrizeRequestToModel(req *contract.UpdateGamePrizeRequest) *models.UpdateGamePrizeRequest {
return &models.UpdateGamePrizeRequest{
Name: req.Name,
Weight: req.Weight,
Stock: req.Stock,
MaxStock: req.MaxStock,
Threshold: req.Threshold,
FallbackPrizeID: req.FallbackPrizeID,
Metadata: req.Metadata,
}
}
func ListGamePrizesRequestToModel(req *contract.ListGamePrizesRequest) *models.ListGamePrizesQuery {
return &models.ListGamePrizesQuery{
Page: req.Page,
Limit: req.Limit,
Search: req.Search,
GameID: req.GameID,
SortBy: req.SortBy,
SortOrder: req.SortOrder,
}
}
func GamePrizeModelToResponse(model *models.GamePrizeResponse) *contract.GamePrizeResponse {
if model == nil {
return nil
}
var game *contract.GameResponse
if model.Game != nil {
game = GameModelToResponse(model.Game)
}
var fallbackPrize *contract.GamePrizeResponse
if model.FallbackPrize != nil {
fallbackPrize = GamePrizeModelToResponse(model.FallbackPrize)
}
return &contract.GamePrizeResponse{
ID: model.ID,
GameID: model.GameID,
Name: model.Name,
Weight: model.Weight,
Stock: model.Stock,
MaxStock: model.MaxStock,
Threshold: model.Threshold,
FallbackPrizeID: model.FallbackPrizeID,
Metadata: model.Metadata,
Game: game,
FallbackPrize: fallbackPrize,
CreatedAt: model.CreatedAt,
UpdatedAt: model.UpdatedAt,
}
}
func GamePrizeModelsToResponses(models []models.GamePrizeResponse) []contract.GamePrizeResponse {
responses := make([]contract.GamePrizeResponse, len(models))
for i, model := range models {
responses[i] = *GamePrizeModelToResponse(&model)
}
return responses
}
func PaginatedGamePrizesResponseToContract(model *models.PaginatedResponse[models.GamePrizeResponse]) *contract.PaginatedGamePrizesResponse {
responses := make([]contract.GamePrizeResponse, len(model.Data))
for i, item := range model.Data {
responses[i] = *GamePrizeModelToResponse(&item)
}
return &contract.PaginatedGamePrizesResponse{
Data: responses,
TotalCount: int(model.Pagination.Total),
Page: model.Pagination.Page,
Limit: model.Pagination.Limit,
TotalPages: model.Pagination.TotalPages,
}
}
// Game Play Transformers
func CreateGamePlayRequestToModel(req *contract.CreateGamePlayRequest) *models.CreateGamePlayRequest {
return &models.CreateGamePlayRequest{
GameID: req.GameID,
CustomerID: req.CustomerID,
CoinsUsed: req.CoinsUsed,
RandomSeed: req.RandomSeed,
}
}
func PlayGameRequestToModel(req *contract.PlayGameRequest) *models.PlayGameRequest {
return &models.PlayGameRequest{
GameID: req.GameID,
CustomerID: req.CustomerID,
}
}
func ListGamePlaysRequestToModel(req *contract.ListGamePlaysRequest) *models.ListGamePlaysQuery {
return &models.ListGamePlaysQuery{
Page: req.Page,
Limit: req.Limit,
Search: req.Search,
GameID: req.GameID,
CustomerID: req.CustomerID,
PrizeID: req.PrizeID,
SortBy: req.SortBy,
SortOrder: req.SortOrder,
}
}
func GamePlayModelToResponse(model *models.GamePlayResponse) *contract.GamePlayResponse {
if model == nil {
return nil
}
var game *contract.GameResponse
if model.Game != nil {
game = GameModelToResponse(model.Game)
}
var customer *contract.CustomerResponse
if model.Customer != nil {
customer = CustomerModelToResponse(model.Customer)
}
var prize *contract.GamePrizeResponse
if model.Prize != nil {
prize = GamePrizeModelToResponse(model.Prize)
}
return &contract.GamePlayResponse{
ID: model.ID,
GameID: model.GameID,
CustomerID: model.CustomerID,
PrizeID: model.PrizeID,
CoinsUsed: model.CoinsUsed,
TokenUsed: model.CoinsUsed,
RandomSeed: model.RandomSeed,
CreatedAt: model.CreatedAt,
Game: game,
Customer: customer,
Prize: prize,
}
}
func PlayGameModelToResponse(model *models.PlayGameResponse) *contract.PlayGameResponse {
if model == nil {
return nil
}
var gamePlay *contract.GamePlayResponse
if &model.GamePlay != nil {
gamePlay = GamePlayModelToResponse(&model.GamePlay)
}
var prizeWon *contract.GamePrizeResponse
if model.PrizeWon != nil {
prizeWon = GamePrizeModelToResponse(model.PrizeWon)
}
var gamePlayValue contract.GamePlayResponse
if gamePlay != nil {
gamePlayValue = *gamePlay
}
return &contract.PlayGameResponse{
GamePlay: gamePlayValue,
PrizeWon: prizeWon,
CoinsRemaining: model.CoinsRemaining,
TokensRemaining: model.CoinsRemaining,
}
}
func PaginatedGamePlaysResponseToContract(model *models.PaginatedResponse[models.GamePlayResponse]) *contract.PaginatedGamePlaysResponse {
responses := make([]contract.GamePlayResponse, len(model.Data))
for i, item := range model.Data {
responses[i] = *GamePlayModelToResponse(&item)
}
return &contract.PaginatedGamePlaysResponse{
Data: responses,
TotalCount: int(model.Pagination.Total),
Page: model.Pagination.Page,
Limit: model.Pagination.Limit,
TotalPages: model.Pagination.TotalPages,
}
}
// Omset Tracker Transformers
func CreateOmsetTrackerRequestToModel(req *contract.CreateOmsetTrackerRequest) *models.CreateOmsetTrackerRequest {
return &models.CreateOmsetTrackerRequest{
PeriodType: req.PeriodType,
PeriodStart: req.PeriodStart,
PeriodEnd: req.PeriodEnd,
Total: req.Total,
GameID: req.GameID,
}
}
func UpdateOmsetTrackerRequestToModel(req *contract.UpdateOmsetTrackerRequest) *models.UpdateOmsetTrackerRequest {
return &models.UpdateOmsetTrackerRequest{
PeriodType: req.PeriodType,
PeriodStart: req.PeriodStart,
PeriodEnd: req.PeriodEnd,
Total: req.Total,
GameID: req.GameID,
}
}
func ListOmsetTrackerRequestToModel(req *contract.ListOmsetTrackerRequest) *models.ListOmsetTrackerQuery {
return &models.ListOmsetTrackerQuery{
Page: req.Page,
Limit: req.Limit,
Search: req.Search,
PeriodType: req.PeriodType,
GameID: req.GameID,
From: req.From,
To: req.To,
SortBy: req.SortBy,
SortOrder: req.SortOrder,
}
}
func OmsetTrackerModelToResponse(model *models.OmsetTrackerResponse) *contract.OmsetTrackerResponse {
if model == nil {
return nil
}
var game *contract.GameResponse
if model.Game != nil {
game = GameModelToResponse(model.Game)
}
return &contract.OmsetTrackerResponse{
ID: model.ID,
PeriodType: model.PeriodType,
PeriodStart: model.PeriodStart,
PeriodEnd: model.PeriodEnd,
Total: model.Total,
GameID: model.GameID,
Game: game,
CreatedAt: model.CreatedAt,
UpdatedAt: model.UpdatedAt,
}
}
func PaginatedOmsetTrackerResponseToContract(model *models.PaginatedResponse[models.OmsetTrackerResponse]) *contract.PaginatedOmsetTrackerResponse {
responses := make([]contract.OmsetTrackerResponse, len(model.Data))
for i, item := range model.Data {
responses[i] = *OmsetTrackerModelToResponse(&item)
}
return &contract.PaginatedOmsetTrackerResponse{
Data: responses,
TotalCount: int(model.Pagination.Total),
Page: model.Pagination.Page,
Limit: model.Pagination.Limit,
TotalPages: model.Pagination.TotalPages,
}
}