feat(enakgame): spin as an EnakGame game; remove the old game flow
EnakGame phase 10 of docs/tasks-enakgame.md (EG-1001 to EG-1003). Spin (EG-1001) - PROBABILITY entries take an optional label (a wheel segment). The customer game list shows a PROBABILITY game's prizes (entry, label, amount, never weights), and completing returns the drawn prize, so the client can draw the wheel and stop it on the server's draw. - docs/enakgame-spin.md: the admin steps to set up spin per organization (no seeder) and the customer app flow. An HTTP test plays it end to end. Old game flow removed (EG-1002) - Routes POST /customer/spin, GET /customer/games, GET /customer/ferris-wheel, and admin /marketing/games, /marketing/game-prizes, /marketing/rewards, with their handlers, services, processors, repositories, validators, models, contracts, mappers and tests (GamePlayProcessor, SpinGameService, rewards, ...). This also closes RFC §15 findings 1 and 2 (double charge, spinning another org's game). - Tables games, game_prizes, game_plays and rewards stay for ledger history. entities.StringSlice moves to its own file; the omset tracker (unrouted) keeps game_id but no longer embeds the old game response. games.is_active dropped (EG-1003) - Migration 000115; nothing reads metadata.coin_cost any more. The EnakPoint integration docs now point at /customer/enakgame. The Postgres tests were not run: no test database here. Migration 000115 has not been run anywhere. The customer app must stop calling the removed endpoints before this is deployed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
296708244e
commit
18e87398bb
@@ -1,58 +0,0 @@
|
||||
package mappers
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/entities"
|
||||
"apskel-pos-be/internal/models"
|
||||
)
|
||||
|
||||
// ToGameResponse converts a game entity to a game response
|
||||
func ToGameResponse(game *entities.Game) *models.GameResponse {
|
||||
if game == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &models.GameResponse{
|
||||
ID: game.ID,
|
||||
Name: game.Name,
|
||||
Type: string(game.Type),
|
||||
IsActive: game.IsActive,
|
||||
Metadata: game.Metadata,
|
||||
CreatedAt: game.CreatedAt,
|
||||
UpdatedAt: game.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// ToGameResponses converts a slice of game entities to game responses
|
||||
func ToGameResponses(games []entities.Game) []models.GameResponse {
|
||||
responses := make([]models.GameResponse, len(games))
|
||||
for i, game := range games {
|
||||
responses[i] = *ToGameResponse(&game)
|
||||
}
|
||||
return responses
|
||||
}
|
||||
|
||||
// ToGameEntity converts a create game request to a game entity
|
||||
func ToGameEntity(req *models.CreateGameRequest) *entities.Game {
|
||||
return &entities.Game{
|
||||
Name: req.Name,
|
||||
Type: entities.GameType(req.Type),
|
||||
IsActive: req.IsActive,
|
||||
Metadata: req.Metadata,
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateGameEntity updates a game entity with update request data
|
||||
func UpdateGameEntity(game *entities.Game, req *models.UpdateGameRequest) {
|
||||
if req.Name != nil {
|
||||
game.Name = *req.Name
|
||||
}
|
||||
if req.Type != nil {
|
||||
game.Type = entities.GameType(*req.Type)
|
||||
}
|
||||
if req.IsActive != nil {
|
||||
game.IsActive = *req.IsActive
|
||||
}
|
||||
if req.Metadata != nil {
|
||||
game.Metadata = req.Metadata
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
package mappers
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/entities"
|
||||
"apskel-pos-be/internal/models"
|
||||
)
|
||||
|
||||
// ToGamePlayResponse converts a game play entity to a game play response
|
||||
func ToGamePlayResponse(gamePlay *entities.GamePlay) *models.GamePlayResponse {
|
||||
if gamePlay == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &models.GamePlayResponse{
|
||||
ID: gamePlay.ID,
|
||||
GameID: gamePlay.GameID,
|
||||
CustomerID: gamePlay.CustomerID,
|
||||
PrizeID: gamePlay.PrizeID,
|
||||
CoinsUsed: gamePlay.CoinsUsed,
|
||||
RandomSeed: gamePlay.RandomSeed,
|
||||
CreatedAt: gamePlay.CreatedAt,
|
||||
Game: ToGameResponse(&gamePlay.Game),
|
||||
Customer: ToCustomerResponse(&gamePlay.Customer),
|
||||
Prize: ToGamePrizeResponse(gamePlay.Prize),
|
||||
}
|
||||
}
|
||||
|
||||
// ToGamePlayResponses converts a slice of game play entities to game play responses
|
||||
func ToGamePlayResponses(gamePlays []entities.GamePlay) []models.GamePlayResponse {
|
||||
responses := make([]models.GamePlayResponse, len(gamePlays))
|
||||
for i, gp := range gamePlays {
|
||||
responses[i] = *ToGamePlayResponse(&gp)
|
||||
}
|
||||
return responses
|
||||
}
|
||||
|
||||
// ToGamePlayResponsesFromPointers converts a slice of game play entity pointers to game play responses
|
||||
func ToGamePlayResponsesFromPointers(gamePlays []*entities.GamePlay) []models.GamePlayResponse {
|
||||
responses := make([]models.GamePlayResponse, len(gamePlays))
|
||||
for i, gp := range gamePlays {
|
||||
responses[i] = *ToGamePlayResponse(gp)
|
||||
}
|
||||
return responses
|
||||
}
|
||||
|
||||
// ToGamePlayEntity converts a create game play request to a game play entity
|
||||
func ToGamePlayEntity(req *models.CreateGamePlayRequest) *entities.GamePlay {
|
||||
return &entities.GamePlay{
|
||||
GameID: req.GameID,
|
||||
CustomerID: req.CustomerID,
|
||||
CoinsUsed: req.CoinsUsed,
|
||||
RandomSeed: req.RandomSeed,
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
package mappers
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/entities"
|
||||
"apskel-pos-be/internal/models"
|
||||
)
|
||||
|
||||
// ToGamePrizeResponse converts a game prize entity to a game prize response
|
||||
func ToGamePrizeResponse(gamePrize *entities.GamePrize) *models.GamePrizeResponse {
|
||||
if gamePrize == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &models.GamePrizeResponse{
|
||||
ID: gamePrize.ID,
|
||||
GameID: gamePrize.GameID,
|
||||
Name: gamePrize.Name,
|
||||
Weight: gamePrize.Weight,
|
||||
Stock: gamePrize.Stock,
|
||||
MaxStock: gamePrize.MaxStock,
|
||||
Threshold: gamePrize.Threshold,
|
||||
FallbackPrizeID: gamePrize.FallbackPrizeID,
|
||||
Image: gamePrize.Image,
|
||||
Metadata: gamePrize.Metadata,
|
||||
Game: ToGameResponse(&gamePrize.Game),
|
||||
FallbackPrize: ToGamePrizeResponse(gamePrize.FallbackPrize),
|
||||
CreatedAt: gamePrize.CreatedAt,
|
||||
UpdatedAt: gamePrize.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// ToGamePrizeResponses converts a slice of game prize entities to game prize responses
|
||||
func ToGamePrizeResponses(gamePrizes []entities.GamePrize) []models.GamePrizeResponse {
|
||||
responses := make([]models.GamePrizeResponse, len(gamePrizes))
|
||||
for i, gp := range gamePrizes {
|
||||
responses[i] = *ToGamePrizeResponse(&gp)
|
||||
}
|
||||
return responses
|
||||
}
|
||||
|
||||
// ToGamePrizeEntity converts a create game prize request to a game prize entity
|
||||
func ToGamePrizeEntity(req *models.CreateGamePrizeRequest) *entities.GamePrize {
|
||||
return &entities.GamePrize{
|
||||
GameID: req.GameID,
|
||||
Name: req.Name,
|
||||
Weight: req.Weight,
|
||||
Stock: req.Stock,
|
||||
MaxStock: req.MaxStock,
|
||||
Threshold: req.Threshold,
|
||||
FallbackPrizeID: req.FallbackPrizeID,
|
||||
Image: req.Image,
|
||||
Metadata: req.Metadata,
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateGamePrizeEntity updates a game prize entity with update request data
|
||||
func UpdateGamePrizeEntity(gamePrize *entities.GamePrize, req *models.UpdateGamePrizeRequest) {
|
||||
if req.Name != nil {
|
||||
gamePrize.Name = *req.Name
|
||||
}
|
||||
if req.Weight != nil {
|
||||
gamePrize.Weight = *req.Weight
|
||||
}
|
||||
if req.Stock != nil {
|
||||
gamePrize.Stock = *req.Stock
|
||||
}
|
||||
if req.MaxStock != nil {
|
||||
gamePrize.MaxStock = req.MaxStock
|
||||
}
|
||||
if req.Threshold != nil {
|
||||
gamePrize.Threshold = req.Threshold
|
||||
}
|
||||
if req.FallbackPrizeID != nil {
|
||||
gamePrize.FallbackPrizeID = req.FallbackPrizeID
|
||||
}
|
||||
if req.Image != nil {
|
||||
gamePrize.Image = req.Image
|
||||
}
|
||||
if req.Metadata != nil {
|
||||
gamePrize.Metadata = req.Metadata
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,6 @@ func ToOmsetTrackerResponse(omsetTracker *entities.OmsetTracker) *models.OmsetTr
|
||||
PeriodEnd: omsetTracker.PeriodEnd,
|
||||
Total: omsetTracker.Total,
|
||||
GameID: omsetTracker.GameID,
|
||||
Game: ToGameResponse(omsetTracker.Game),
|
||||
CreatedAt: omsetTracker.CreatedAt,
|
||||
UpdatedAt: omsetTracker.UpdatedAt,
|
||||
}
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
package mappers
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/contract"
|
||||
"apskel-pos-be/internal/entities"
|
||||
"apskel-pos-be/internal/models"
|
||||
)
|
||||
|
||||
func ToRewardResponse(entity *entities.Reward) *models.RewardResponse {
|
||||
if entity == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var tnc *models.TermsAndConditionsStruct
|
||||
if entity.Tnc != nil {
|
||||
var sections []models.TncSectionStruct
|
||||
for _, section := range entity.Tnc.Sections {
|
||||
sections = append(sections, models.TncSectionStruct{
|
||||
Title: section.Title,
|
||||
Rules: section.Rules,
|
||||
})
|
||||
}
|
||||
tnc = &models.TermsAndConditionsStruct{
|
||||
Sections: sections,
|
||||
ExpiryDays: entity.Tnc.ExpiryDays,
|
||||
}
|
||||
}
|
||||
|
||||
var images *[]string
|
||||
if entity.Images != nil {
|
||||
imagesSlice := []string(*entity.Images)
|
||||
images = &imagesSlice
|
||||
}
|
||||
|
||||
return &models.RewardResponse{
|
||||
ID: entity.ID,
|
||||
Name: entity.Name,
|
||||
RewardType: string(entity.RewardType),
|
||||
CostPoints: entity.CostPoints,
|
||||
Stock: entity.Stock,
|
||||
MaxPerCustomer: entity.MaxPerCustomer,
|
||||
Tnc: tnc,
|
||||
Metadata: entity.Metadata,
|
||||
Images: images,
|
||||
CreatedAt: entity.CreatedAt,
|
||||
UpdatedAt: entity.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func ToRewardEntity(request *contract.CreateRewardRequest) *entities.Reward {
|
||||
if request == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var tnc *entities.TermsAndConditions
|
||||
if request.Tnc != nil {
|
||||
var sections []entities.TncSection
|
||||
for _, section := range request.Tnc.Sections {
|
||||
sections = append(sections, entities.TncSection{
|
||||
Title: section.Title,
|
||||
Rules: section.Rules,
|
||||
})
|
||||
}
|
||||
tnc = &entities.TermsAndConditions{
|
||||
Sections: sections,
|
||||
ExpiryDays: request.Tnc.ExpiryDays,
|
||||
}
|
||||
}
|
||||
|
||||
var images *entities.StringSlice
|
||||
if request.Images != nil {
|
||||
stringSlice := entities.StringSlice(*request.Images)
|
||||
images = &stringSlice
|
||||
}
|
||||
|
||||
return &entities.Reward{
|
||||
Name: request.Name,
|
||||
RewardType: entities.RewardType(request.RewardType),
|
||||
CostPoints: request.CostPoints,
|
||||
Stock: request.Stock,
|
||||
MaxPerCustomer: request.MaxPerCustomer,
|
||||
Tnc: tnc,
|
||||
Metadata: request.Metadata,
|
||||
Images: images,
|
||||
}
|
||||
}
|
||||
|
||||
func ToRewardEntityFromUpdate(request *contract.UpdateRewardRequest) *entities.Reward {
|
||||
if request == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var tnc *entities.TermsAndConditions
|
||||
if request.Tnc != nil {
|
||||
var sections []entities.TncSection
|
||||
for _, section := range request.Tnc.Sections {
|
||||
sections = append(sections, entities.TncSection{
|
||||
Title: section.Title,
|
||||
Rules: section.Rules,
|
||||
})
|
||||
}
|
||||
tnc = &entities.TermsAndConditions{
|
||||
Sections: sections,
|
||||
ExpiryDays: request.Tnc.ExpiryDays,
|
||||
}
|
||||
}
|
||||
|
||||
var images *entities.StringSlice
|
||||
if request.Images != nil {
|
||||
stringSlice := entities.StringSlice(*request.Images)
|
||||
images = &stringSlice
|
||||
}
|
||||
|
||||
return &entities.Reward{
|
||||
ID: request.ID,
|
||||
Name: request.Name,
|
||||
RewardType: entities.RewardType(request.RewardType),
|
||||
CostPoints: request.CostPoints,
|
||||
Stock: request.Stock,
|
||||
MaxPerCustomer: request.MaxPerCustomer,
|
||||
Tnc: tnc,
|
||||
Metadata: request.Metadata,
|
||||
Images: images,
|
||||
}
|
||||
}
|
||||
|
||||
func ToRewardResponseSlice(entities []entities.Reward) []models.RewardResponse {
|
||||
if entities == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
responses := make([]models.RewardResponse, len(entities))
|
||||
for i, entity := range entities {
|
||||
responses[i] = *ToRewardResponse(&entity)
|
||||
}
|
||||
|
||||
return responses
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
package mappers
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/contract"
|
||||
"apskel-pos-be/internal/models"
|
||||
)
|
||||
|
||||
// SpinGameRequestContractToModel converts spin game request from contract to model
|
||||
func SpinGameRequestContractToModel(req *contract.SpinGameRequest) *models.SpinGameRequest {
|
||||
if req == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &models.SpinGameRequest{
|
||||
SpinID: req.SpinID,
|
||||
}
|
||||
}
|
||||
|
||||
// SpinGameResponseModelToContract converts spin game response from model to contract
|
||||
func SpinGameResponseModelToContract(resp *models.SpinGameResponse) *contract.SpinGameResponse {
|
||||
if resp == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &contract.SpinGameResponse{
|
||||
Status: resp.Status,
|
||||
Message: resp.Message,
|
||||
Data: SpinGameResponseDataModelToContract(resp.Data),
|
||||
}
|
||||
}
|
||||
|
||||
// SpinGameResponseDataModelToContract converts spin game response data from model to contract
|
||||
func SpinGameResponseDataModelToContract(data *models.SpinGameResponseData) *contract.SpinGameResponseData {
|
||||
if data == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &contract.SpinGameResponseData{
|
||||
GamePlay: GamePlayResponseModelToContract(&data.GamePlay),
|
||||
PrizeWon: CustomerGamePrizeResponseModelToContract(data.PrizeWon),
|
||||
CoinsRemaining: data.CoinsRemaining,
|
||||
}
|
||||
}
|
||||
|
||||
// GamePlayResponseModelToContract converts game play response from model to contract
|
||||
func GamePlayResponseModelToContract(resp *models.GamePlayResponse) contract.GamePlayResponse {
|
||||
if resp == nil {
|
||||
return contract.GamePlayResponse{}
|
||||
}
|
||||
|
||||
return contract.GamePlayResponse{
|
||||
ID: resp.ID,
|
||||
GameID: resp.GameID,
|
||||
CustomerID: resp.CustomerID,
|
||||
PrizeID: resp.PrizeID,
|
||||
CoinsUsed: resp.CoinsUsed,
|
||||
RandomSeed: resp.RandomSeed,
|
||||
CreatedAt: resp.CreatedAt,
|
||||
Game: nil, // Optional field - can be populated separately if needed
|
||||
Customer: nil, // Optional field - can be populated separately if needed
|
||||
Prize: nil, // Optional field - can be populated separately if needed
|
||||
}
|
||||
}
|
||||
|
||||
// CustomerGamePrizeResponseModelToContract converts customer game prize response from model to contract
|
||||
func CustomerGamePrizeResponseModelToContract(resp *models.CustomerGamePrizeResponse) *contract.CustomerGamePrizeResponse {
|
||||
if resp == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &contract.CustomerGamePrizeResponse{
|
||||
ID: resp.ID,
|
||||
GameID: resp.GameID,
|
||||
Name: resp.Name,
|
||||
Image: resp.Image,
|
||||
Metadata: resp.Metadata,
|
||||
CreatedAt: resp.CreatedAt,
|
||||
UpdatedAt: resp.UpdatedAt,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user