refactor(loyalty): remove what is left of tokens
Tokens are EnakCoin and no app uses the token names any more, so their compatibility layer goes: - GET /customer/tokens and its handler, service, processor and response types. - total_tokens and tokens_history on GET /customer/wallet; last_updated now comes from the most recent row of either currency. - token_used and tokens_remaining on game and spin responses, and sort_by=token_used on the game play list. - TOKENS as a campaign type and reward type, with the mapping to COINS: migration 000092 already renamed the stored values. The customer_tokens table and its entity stay, as cmd/wallet-migrate still reads them, and LEGACY_TOKENS stays as the reference of the MIGRATION rows it wrote. The docs list the removed names and their replacements. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
9b21af8892
commit
c988a79d3b
@@ -52,7 +52,6 @@ const (
|
||||
TableEntity = "table"
|
||||
// Gamification entities
|
||||
CustomerPointsEntity = "customer_points"
|
||||
CustomerTokensEntity = "customer_tokens"
|
||||
TierEntity = "tier"
|
||||
GameEntity = "game"
|
||||
GamePrizeEntity = "game_prize"
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
type CreateCampaignRequest struct {
|
||||
Name string `json:"name" binding:"required,min=1,max=150"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Type string `json:"type" binding:"required,oneof=REWARD POINTS COINS TOKENS MIXED"`
|
||||
Type string `json:"type" binding:"required,oneof=REWARD POINTS COINS MIXED"`
|
||||
StartDate time.Time `json:"start_date" binding:"required"`
|
||||
EndDate time.Time `json:"end_date" binding:"required"`
|
||||
IsActive bool `json:"is_active"`
|
||||
@@ -25,7 +25,7 @@ type UpdateCampaignRequest struct {
|
||||
ID uuid.UUID `json:"id" binding:"required"`
|
||||
Name string `json:"name" binding:"required,min=1,max=150"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Type string `json:"type" binding:"required,oneof=REWARD POINTS COINS TOKENS MIXED"`
|
||||
Type string `json:"type" binding:"required,oneof=REWARD POINTS COINS MIXED"`
|
||||
StartDate time.Time `json:"start_date" binding:"required"`
|
||||
EndDate time.Time `json:"end_date" binding:"required"`
|
||||
IsActive bool `json:"is_active"`
|
||||
@@ -58,7 +58,7 @@ type CreateCampaignRuleRequest struct {
|
||||
CampaignID uuid.UUID `json:"campaign_id" binding:"required"`
|
||||
RuleType string `json:"rule_type" binding:"required,oneof=TIER SPEND PRODUCT CATEGORY DAY LOCATION"`
|
||||
ConditionValue *string `json:"condition_value,omitempty"`
|
||||
RewardType string `json:"reward_type" binding:"required,oneof=POINTS COINS TOKENS REWARD"`
|
||||
RewardType string `json:"reward_type" binding:"required,oneof=POINTS COINS REWARD"`
|
||||
RewardValue *int64 `json:"reward_value,omitempty"`
|
||||
RewardSubtype *string `json:"reward_subtype,omitempty"`
|
||||
RewardRefID *uuid.UUID `json:"reward_ref_id,omitempty"`
|
||||
@@ -70,7 +70,7 @@ type UpdateCampaignRuleRequest struct {
|
||||
CampaignID uuid.UUID `json:"campaign_id" binding:"required"`
|
||||
RuleType string `json:"rule_type" binding:"required,oneof=TIER SPEND PRODUCT CATEGORY DAY LOCATION"`
|
||||
ConditionValue *string `json:"condition_value,omitempty"`
|
||||
RewardType string `json:"reward_type" binding:"required,oneof=POINTS COINS TOKENS REWARD"`
|
||||
RewardType string `json:"reward_type" binding:"required,oneof=POINTS COINS REWARD"`
|
||||
RewardValue *int64 `json:"reward_value,omitempty"`
|
||||
RewardSubtype *string `json:"reward_subtype,omitempty"`
|
||||
RewardRefID *uuid.UUID `json:"reward_ref_id,omitempty"`
|
||||
@@ -142,7 +142,7 @@ type ListCampaignRulesResponse struct {
|
||||
type CampaignRuleStruct struct {
|
||||
RuleType string `json:"rule_type" binding:"required,oneof=TIER SPEND PRODUCT CATEGORY DAY LOCATION"`
|
||||
ConditionValue *string `json:"condition_value,omitempty"`
|
||||
RewardType string `json:"reward_type" binding:"required,oneof=POINTS COINS TOKENS REWARD"`
|
||||
RewardType string `json:"reward_type" binding:"required,oneof=POINTS COINS REWARD"`
|
||||
RewardValue *int64 `json:"reward_value,omitempty"`
|
||||
RewardSubtype *string `json:"reward_subtype,omitempty"`
|
||||
RewardRefID *uuid.UUID `json:"reward_ref_id,omitempty"`
|
||||
|
||||
@@ -25,26 +25,6 @@ type PointsHistoryItem struct {
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type GetCustomerTokensResponse struct {
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
Data *GetCustomerTokensResponseData `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
type GetCustomerTokensResponseData struct {
|
||||
TotalTokens int64 `json:"total_tokens"`
|
||||
TokensHistory []TokensHistoryItem `json:"tokens_history,omitempty"`
|
||||
LastUpdated time.Time `json:"last_updated"`
|
||||
}
|
||||
|
||||
type TokensHistoryItem struct {
|
||||
ID string `json:"id"`
|
||||
Tokens int64 `json:"tokens"`
|
||||
Type string `json:"type"` // EARNED, REDEEMED, EXPIRED
|
||||
Description string `json:"description"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type GetCustomerWalletResponse struct {
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
@@ -53,8 +33,6 @@ type GetCustomerWalletResponse struct {
|
||||
|
||||
type GetCustomerWalletResponseData struct {
|
||||
TotalPoints int64 `json:"total_points"`
|
||||
TotalTokens int64 `json:"total_tokens"`
|
||||
PointsHistory []PointsHistoryItem `json:"points_history,omitempty"`
|
||||
TokensHistory []TokensHistoryItem `json:"tokens_history,omitempty"`
|
||||
LastUpdated time.Time `json:"last_updated"`
|
||||
}
|
||||
|
||||
@@ -14,13 +14,11 @@ type CreateGamePlayRequest struct {
|
||||
}
|
||||
|
||||
type GamePlayResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
GameID uuid.UUID `json:"game_id"`
|
||||
CustomerID uuid.UUID `json:"customer_id"`
|
||||
PrizeID *uuid.UUID `json:"prize_id,omitempty"`
|
||||
CoinsUsed int `json:"coins_used"`
|
||||
// Deprecated: the same as CoinsUsed, kept until the apps read coins_used.
|
||||
TokenUsed int `json:"token_used"`
|
||||
ID uuid.UUID `json:"id"`
|
||||
GameID uuid.UUID `json:"game_id"`
|
||||
CustomerID uuid.UUID `json:"customer_id"`
|
||||
PrizeID *uuid.UUID `json:"prize_id,omitempty"`
|
||||
CoinsUsed int `json:"coins_used"`
|
||||
RandomSeed *string `json:"random_seed,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Game *GameResponse `json:"game,omitempty"`
|
||||
@@ -35,7 +33,7 @@ type ListGamePlaysRequest struct {
|
||||
GameID *uuid.UUID `json:"game_id"`
|
||||
CustomerID *uuid.UUID `json:"customer_id"`
|
||||
PrizeID *uuid.UUID `json:"prize_id"`
|
||||
SortBy string `json:"sort_by" validate:"omitempty,oneof=created_at coins_used token_used"`
|
||||
SortBy string `json:"sort_by" validate:"omitempty,oneof=created_at coins_used"`
|
||||
SortOrder string `json:"sort_order" validate:"omitempty,oneof=asc desc"`
|
||||
}
|
||||
|
||||
@@ -58,6 +56,4 @@ type PlayGameResponse struct {
|
||||
GamePlay GamePlayResponse `json:"game_play"`
|
||||
PrizeWon *GamePrizeResponse `json:"prize_won,omitempty"`
|
||||
CoinsRemaining int64 `json:"coins_remaining"`
|
||||
// Deprecated: the same as CoinsRemaining, kept until the apps read coins_remaining.
|
||||
TokensRemaining int64 `json:"tokens_remaining"`
|
||||
}
|
||||
|
||||
@@ -17,6 +17,4 @@ type SpinGameResponseData struct {
|
||||
GamePlay GamePlayResponse `json:"game_play"`
|
||||
PrizeWon *CustomerGamePrizeResponse `json:"prize_won,omitempty"`
|
||||
CoinsRemaining int64 `json:"coins_remaining"`
|
||||
// Deprecated: the same as CoinsRemaining, kept until the apps read coins_remaining.
|
||||
TokensRemaining int64 `json:"tokens_remaining"`
|
||||
}
|
||||
|
||||
@@ -16,27 +16,6 @@ const (
|
||||
CampaignTypeMixed CampaignType = "MIXED"
|
||||
)
|
||||
|
||||
// legacyCampaignTokens is what COINS was called before tokens became EnakCoin
|
||||
// (docs/prd-point-coin.md §10). Requests may still send it; it is stored as COINS.
|
||||
const legacyCampaignTokens = "TOKENS"
|
||||
|
||||
// NormalizeCampaignType maps the legacy TOKENS to COINS and leaves anything else as is.
|
||||
func NormalizeCampaignType(t string) CampaignType {
|
||||
if t == legacyCampaignTokens {
|
||||
return CampaignTypeCoins
|
||||
}
|
||||
return CampaignType(t)
|
||||
}
|
||||
|
||||
// NormalizeCampaignRewardType maps the legacy TOKENS to COINS and leaves anything
|
||||
// else as is.
|
||||
func NormalizeCampaignRewardType(t string) CampaignRewardType {
|
||||
if t == legacyCampaignTokens {
|
||||
return CampaignRewardTypeCoins
|
||||
}
|
||||
return CampaignRewardType(t)
|
||||
}
|
||||
|
||||
type RuleType string
|
||||
|
||||
const (
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNormalizeCampaignTypesMapsLegacyTokensToCoins(t *testing.T) {
|
||||
assert.Equal(t, CampaignTypeCoins, NormalizeCampaignType("TOKENS"))
|
||||
assert.Equal(t, CampaignTypeCoins, NormalizeCampaignType("COINS"))
|
||||
assert.Equal(t, CampaignTypePoints, NormalizeCampaignType("POINTS"))
|
||||
assert.Equal(t, CampaignType(""), NormalizeCampaignType(""), "an empty filter stays empty")
|
||||
|
||||
assert.Equal(t, CampaignRewardTypeCoins, NormalizeCampaignRewardType("TOKENS"))
|
||||
assert.Equal(t, CampaignRewardTypeReward, NormalizeCampaignRewardType("REWARD"))
|
||||
assert.Equal(t, CampaignRewardType(""), NormalizeCampaignRewardType(""))
|
||||
}
|
||||
@@ -59,40 +59,6 @@ func (h *CustomerPointsHandler) GetCustomerPoints(c *gin.Context) {
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "CustomerPointsHandler::GetCustomerPoints")
|
||||
}
|
||||
|
||||
func (h *CustomerPointsHandler) GetCustomerTokens(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
// Get customer ID from context (set by middleware)
|
||||
customerID, exists := c.Get("customer_id")
|
||||
if !exists {
|
||||
logger.FromContext(ctx).Error("Customer ID not found in context")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||
contract.NewResponseError(constants.ValidationErrorCode, constants.AuthHandlerEntity, "Customer ID not found"),
|
||||
}), "CustomerPointsHandler::GetCustomerTokens")
|
||||
return
|
||||
}
|
||||
|
||||
customerIDStr, ok := customerID.(string)
|
||||
if !ok {
|
||||
logger.FromContext(ctx).Error("Invalid customer ID type in context")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||
contract.NewResponseError(constants.ValidationErrorCode, constants.AuthHandlerEntity, "Invalid customer ID"),
|
||||
}), "CustomerPointsHandler::GetCustomerTokens")
|
||||
return
|
||||
}
|
||||
|
||||
response, err := h.customerPointsService.GetCustomerTokens(ctx, customerIDStr)
|
||||
if err != nil {
|
||||
logger.FromContext(ctx).WithError(err).Error("CustomerPointsHandler::GetCustomerTokens -> service call failed")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||
contract.NewResponseError(walletErrorCode(err), constants.RequestEntity, err.Error()),
|
||||
}), "CustomerPointsHandler::GetCustomerTokens")
|
||||
return
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "CustomerPointsHandler::GetCustomerTokens")
|
||||
}
|
||||
|
||||
func (h *CustomerPointsHandler) GetCustomerWallet(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ import (
|
||||
)
|
||||
|
||||
// Drives the customer wallet endpoints over HTTP down to Postgres, to show the app
|
||||
// gets the right numbers from the new endpoints and from the old /points, /tokens
|
||||
// gets the right numbers from the new endpoints and from the old /points
|
||||
// and /wallet shapes. Needs TEST_DATABASE_URL pointing at a migrated database; see
|
||||
// internal/repository/wallet_repository_test.go.
|
||||
func TestCustomerWalletEndpoints_AgainstPostgres(t *testing.T) {
|
||||
@@ -111,7 +111,6 @@ func TestCustomerWalletEndpoints_AgainstPostgres(t *testing.T) {
|
||||
}{{"/a", customer}, {"/b", other}, {"/gone", uuid.New()}} {
|
||||
g := router.Group(who.prefix, as(who.id))
|
||||
g.GET("/points", h.GetCustomerPoints)
|
||||
g.GET("/tokens", h.GetCustomerTokens)
|
||||
g.GET("/wallet", h.GetCustomerWallet)
|
||||
g.GET("/wallet/transactions", h.GetCustomerWalletTransactions)
|
||||
}
|
||||
@@ -128,16 +127,12 @@ func TestCustomerWalletEndpoints_AgainstPostgres(t *testing.T) {
|
||||
return body["data"].(map[string]any)["data"].(map[string]any)
|
||||
}
|
||||
|
||||
// Old app: /points and /tokens keep their shape and show the spendable balance.
|
||||
// Old app: /points keeps its shape and shows the spendable balance.
|
||||
status, body := get("/a/points")
|
||||
require.Equal(t, http.StatusOK, status, body)
|
||||
assert.EqualValues(t, 250, inner(body)["total_points"], "300 - 50; the expired 20 is not spendable")
|
||||
assert.Len(t, inner(body)["points_history"], 3)
|
||||
|
||||
status, body = get("/a/tokens")
|
||||
require.Equal(t, http.StatusOK, status, body)
|
||||
assert.EqualValues(t, 8, inner(body)["total_tokens"])
|
||||
|
||||
// /wallet: new fields, with the old ones still there.
|
||||
status, body = get("/a/wallet")
|
||||
require.Equal(t, http.StatusOK, status, body)
|
||||
@@ -151,9 +146,10 @@ func TestCustomerWalletEndpoints_AgainstPostgres(t *testing.T) {
|
||||
assert.Equal(t, map[string]any{"amount": float64(250), "date": time.Date(next, 7, 2, 0, 0, 0, 0, time.UTC).Format("2006-01-02")},
|
||||
nearest["point"], "both lots fall on 2 July in Jakarta although they are on different UTC days")
|
||||
assert.Nil(t, nearest["coin"])
|
||||
for _, legacy := range []string{"total_points", "total_tokens", "points_history", "tokens_history", "last_updated"} {
|
||||
for _, legacy := range []string{"total_points", "points_history", "last_updated"} {
|
||||
assert.Contains(t, w, legacy)
|
||||
}
|
||||
assert.NotContains(t, w, "total_tokens", "tokens are gone; EnakCoin is coin_balance")
|
||||
assert.EqualValues(t, 250, w["total_points"])
|
||||
|
||||
recent := w["recent_transactions"].([]any)
|
||||
|
||||
@@ -71,7 +71,7 @@ func ToCampaignEntity(request *contract.CreateCampaignRequest) *entities.Campaig
|
||||
return &entities.Campaign{
|
||||
Name: request.Name,
|
||||
Description: request.Description,
|
||||
Type: entities.NormalizeCampaignType(request.Type),
|
||||
Type: entities.CampaignType(request.Type),
|
||||
StartDate: request.StartDate,
|
||||
EndDate: request.EndDate,
|
||||
IsActive: request.IsActive,
|
||||
@@ -91,7 +91,7 @@ func ToCampaignEntityFromUpdate(request *contract.UpdateCampaignRequest) *entiti
|
||||
ID: request.ID,
|
||||
Name: request.Name,
|
||||
Description: request.Description,
|
||||
Type: entities.NormalizeCampaignType(request.Type),
|
||||
Type: entities.CampaignType(request.Type),
|
||||
StartDate: request.StartDate,
|
||||
EndDate: request.EndDate,
|
||||
IsActive: request.IsActive,
|
||||
@@ -118,7 +118,7 @@ func ToCampaignRuleEntity(request *contract.CampaignRuleStruct, campaignID uuid.
|
||||
CampaignID: campaignID,
|
||||
RuleType: entities.RuleType(request.RuleType),
|
||||
ConditionValue: request.ConditionValue,
|
||||
RewardType: entities.NormalizeCampaignRewardType(request.RewardType),
|
||||
RewardType: entities.CampaignRewardType(request.RewardType),
|
||||
RewardValue: request.RewardValue,
|
||||
RewardSubtype: rewardSubtype,
|
||||
RewardRefID: request.RewardRefID,
|
||||
@@ -142,7 +142,7 @@ func ToCampaignRuleEntityFromUpdate(request *contract.CampaignRuleStruct, campai
|
||||
CampaignID: campaignID,
|
||||
RuleType: entities.RuleType(request.RuleType),
|
||||
ConditionValue: request.ConditionValue,
|
||||
RewardType: entities.NormalizeCampaignRewardType(request.RewardType),
|
||||
RewardType: entities.CampaignRewardType(request.RewardType),
|
||||
RewardValue: request.RewardValue,
|
||||
RewardSubtype: rewardSubtype,
|
||||
RewardRefID: request.RewardRefID,
|
||||
|
||||
@@ -36,10 +36,9 @@ func SpinGameResponseDataModelToContract(data *models.SpinGameResponseData) *con
|
||||
}
|
||||
|
||||
return &contract.SpinGameResponseData{
|
||||
GamePlay: GamePlayResponseModelToContract(&data.GamePlay),
|
||||
PrizeWon: CustomerGamePrizeResponseModelToContract(data.PrizeWon),
|
||||
CoinsRemaining: data.CoinsRemaining,
|
||||
TokensRemaining: data.CoinsRemaining,
|
||||
GamePlay: GamePlayResponseModelToContract(&data.GamePlay),
|
||||
PrizeWon: CustomerGamePrizeResponseModelToContract(data.PrizeWon),
|
||||
CoinsRemaining: data.CoinsRemaining,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +54,6 @@ func GamePlayResponseModelToContract(resp *models.GamePlayResponse) contract.Gam
|
||||
CustomerID: resp.CustomerID,
|
||||
PrizeID: resp.PrizeID,
|
||||
CoinsUsed: resp.CoinsUsed,
|
||||
TokenUsed: resp.CoinsUsed,
|
||||
RandomSeed: resp.RandomSeed,
|
||||
CreatedAt: resp.CreatedAt,
|
||||
Game: nil, // Optional field - can be populated separately if needed
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
type CreateCampaignRequest struct {
|
||||
Name string `json:"name" binding:"required,min=1,max=150"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Type string `json:"type" binding:"required,oneof=REWARD POINTS COINS TOKENS MIXED"`
|
||||
Type string `json:"type" binding:"required,oneof=REWARD POINTS COINS MIXED"`
|
||||
StartDate time.Time `json:"start_date" binding:"required"`
|
||||
EndDate time.Time `json:"end_date" binding:"required"`
|
||||
IsActive bool `json:"is_active"`
|
||||
@@ -25,7 +25,7 @@ type UpdateCampaignRequest struct {
|
||||
ID uuid.UUID `json:"id" binding:"required"`
|
||||
Name string `json:"name" binding:"required,min=1,max=150"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Type string `json:"type" binding:"required,oneof=REWARD POINTS COINS TOKENS MIXED"`
|
||||
Type string `json:"type" binding:"required,oneof=REWARD POINTS COINS MIXED"`
|
||||
StartDate time.Time `json:"start_date" binding:"required"`
|
||||
EndDate time.Time `json:"end_date" binding:"required"`
|
||||
IsActive bool `json:"is_active"`
|
||||
@@ -88,7 +88,7 @@ type CreateCampaignRuleRequest struct {
|
||||
CampaignID uuid.UUID `json:"campaign_id" binding:"required"`
|
||||
RuleType string `json:"rule_type" binding:"required,oneof=TIER SPEND PRODUCT CATEGORY DAY LOCATION"`
|
||||
ConditionValue *string `json:"condition_value,omitempty"`
|
||||
RewardType string `json:"reward_type" binding:"required,oneof=POINTS COINS TOKENS REWARD"`
|
||||
RewardType string `json:"reward_type" binding:"required,oneof=POINTS COINS REWARD"`
|
||||
RewardValue *int64 `json:"reward_value,omitempty"`
|
||||
RewardSubtype *string `json:"reward_subtype,omitempty"`
|
||||
RewardRefID *uuid.UUID `json:"reward_ref_id,omitempty"`
|
||||
@@ -100,7 +100,7 @@ type UpdateCampaignRuleRequest struct {
|
||||
CampaignID uuid.UUID `json:"campaign_id" binding:"required"`
|
||||
RuleType string `json:"rule_type" binding:"required,oneof=TIER SPEND PRODUCT CATEGORY DAY LOCATION"`
|
||||
ConditionValue *string `json:"condition_value,omitempty"`
|
||||
RewardType string `json:"reward_type" binding:"required,oneof=POINTS COINS TOKENS REWARD"`
|
||||
RewardType string `json:"reward_type" binding:"required,oneof=POINTS COINS REWARD"`
|
||||
RewardValue *int64 `json:"reward_value,omitempty"`
|
||||
RewardSubtype *string `json:"reward_subtype,omitempty"`
|
||||
RewardRefID *uuid.UUID `json:"reward_ref_id,omitempty"`
|
||||
@@ -126,7 +126,7 @@ type ListCampaignRulesResponse struct {
|
||||
type CampaignRuleStruct struct {
|
||||
RuleType string `json:"rule_type" binding:"required,oneof=TIER SPEND PRODUCT CATEGORY DAY LOCATION"`
|
||||
ConditionValue *string `json:"condition_value,omitempty"`
|
||||
RewardType string `json:"reward_type" binding:"required,oneof=POINTS COINS TOKENS REWARD"`
|
||||
RewardType string `json:"reward_type" binding:"required,oneof=POINTS COINS REWARD"`
|
||||
RewardValue *int64 `json:"reward_value,omitempty"`
|
||||
RewardSubtype *string `json:"reward_subtype,omitempty"`
|
||||
RewardRefID *uuid.UUID `json:"reward_ref_id,omitempty"`
|
||||
|
||||
@@ -25,26 +25,6 @@ type PointsHistoryItem struct {
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type GetCustomerTokensResponse struct {
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
Data *GetCustomerTokensResponseData `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
type GetCustomerTokensResponseData struct {
|
||||
TotalTokens int64 `json:"total_tokens"`
|
||||
TokensHistory []TokensHistoryItem `json:"tokens_history,omitempty"`
|
||||
LastUpdated time.Time `json:"last_updated"`
|
||||
}
|
||||
|
||||
type TokensHistoryItem struct {
|
||||
ID string `json:"id"`
|
||||
Tokens int64 `json:"tokens"`
|
||||
Type string `json:"type"` // EARNED, REDEEMED, EXPIRED
|
||||
Description string `json:"description"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type GetCustomerWalletResponse struct {
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
@@ -66,8 +46,6 @@ type GetCustomerWalletResponseData struct {
|
||||
// Deprecated: the shape of this endpoint before the wallet, kept for app versions
|
||||
// that still read it. Filled from the wallet like the fields above.
|
||||
TotalPoints int64 `json:"total_points"`
|
||||
TotalTokens int64 `json:"total_tokens"`
|
||||
PointsHistory []PointsHistoryItem `json:"points_history,omitempty"`
|
||||
TokensHistory []TokensHistoryItem `json:"tokens_history,omitempty"`
|
||||
LastUpdated time.Time `json:"last_updated"`
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ type ListGamePlaysQuery struct {
|
||||
GameID *uuid.UUID `query:"game_id"`
|
||||
CustomerID *uuid.UUID `query:"customer_id"`
|
||||
PrizeID *uuid.UUID `query:"prize_id"`
|
||||
SortBy string `query:"sort_by" validate:"omitempty,oneof=created_at coins_used token_used"`
|
||||
SortBy string `query:"sort_by" validate:"omitempty,oneof=created_at coins_used"`
|
||||
SortOrder string `query:"sort_order" validate:"omitempty,oneof=asc desc"`
|
||||
}
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ func (p *campaignProcessor) ListCampaigns(ctx context.Context, req *contract.Lis
|
||||
Page: req.Page,
|
||||
Limit: req.Limit,
|
||||
Search: req.Search,
|
||||
Type: string(entities.NormalizeCampaignType(req.Type)),
|
||||
Type: req.Type,
|
||||
IsActive: req.IsActive,
|
||||
ShowOnApp: req.ShowOnApp,
|
||||
StartDate: req.StartDate,
|
||||
@@ -178,7 +178,7 @@ func (p *campaignRuleProcessor) CreateCampaignRule(ctx context.Context, req *con
|
||||
CampaignID: req.CampaignID,
|
||||
RuleType: entities.RuleType(req.RuleType),
|
||||
ConditionValue: req.ConditionValue,
|
||||
RewardType: entities.NormalizeCampaignRewardType(req.RewardType),
|
||||
RewardType: entities.CampaignRewardType(req.RewardType),
|
||||
RewardValue: req.RewardValue,
|
||||
RewardSubtype: (*entities.RewardSubtype)(req.RewardSubtype),
|
||||
RewardRefID: req.RewardRefID,
|
||||
@@ -218,7 +218,7 @@ func (p *campaignRuleProcessor) ListCampaignRules(ctx context.Context, req *cont
|
||||
Limit: req.Limit,
|
||||
CampaignID: req.CampaignID,
|
||||
RuleType: req.RuleType,
|
||||
RewardType: string(entities.NormalizeCampaignRewardType(req.RewardType)),
|
||||
RewardType: req.RewardType,
|
||||
}
|
||||
|
||||
// Get from repository
|
||||
@@ -247,7 +247,7 @@ func (p *campaignRuleProcessor) UpdateCampaignRule(ctx context.Context, req *con
|
||||
CampaignID: req.CampaignID,
|
||||
RuleType: entities.RuleType(req.RuleType),
|
||||
ConditionValue: req.ConditionValue,
|
||||
RewardType: entities.NormalizeCampaignRewardType(req.RewardType),
|
||||
RewardType: entities.CampaignRewardType(req.RewardType),
|
||||
RewardValue: req.RewardValue,
|
||||
RewardSubtype: (*entities.RewardSubtype)(req.RewardSubtype),
|
||||
RewardRefID: req.RewardRefID,
|
||||
|
||||
@@ -22,7 +22,7 @@ func NewCustomerPointsProcessor(walletQuery *WalletQueryProcessor, gameRepo *rep
|
||||
}
|
||||
}
|
||||
|
||||
// The three endpoints below are the customer app's balance screens. They read the
|
||||
// The endpoints below are the customer app's balance screens. They read the
|
||||
// EnakPoint & EnakCoin wallet; customer_points and customer_tokens are no longer used
|
||||
// for balances (docs/prd-point-coin.md §9).
|
||||
|
||||
@@ -42,22 +42,6 @@ func (p *CustomerPointsProcessor) GetCustomerTotalPointsAPI(ctx context.Context,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *CustomerPointsProcessor) GetCustomerTotalTokensAPI(ctx context.Context, customerID string) (*models.GetCustomerTokensResponse, error) {
|
||||
id, err := parseWalletCustomerID(customerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data, err := p.walletQuery.LegacyTokens(ctx, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get customer tokens: %w", err)
|
||||
}
|
||||
return &models.GetCustomerTokensResponse{
|
||||
Status: "SUCCESS",
|
||||
Message: "Customer tokens retrieved successfully.",
|
||||
Data: data,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *CustomerPointsProcessor) GetCustomerWalletAPI(ctx context.Context, customerID string) (*models.GetCustomerWalletResponse, error) {
|
||||
id, err := parseWalletCustomerID(customerID)
|
||||
if err != nil {
|
||||
|
||||
@@ -99,10 +99,6 @@ func (p *GamePlayProcessor) ListGamePlays(ctx context.Context, query *models.Lis
|
||||
}
|
||||
|
||||
offset := (query.Page - 1) * query.Limit
|
||||
// The column was renamed; the old name still sorts the same way.
|
||||
if query.SortBy == "token_used" {
|
||||
query.SortBy = "coins_used"
|
||||
}
|
||||
|
||||
// Get game plays from repository
|
||||
gamePlays, total, err := p.gamePlayRepo.List(
|
||||
|
||||
@@ -82,10 +82,6 @@ func (p *WalletQueryProcessor) Summary(ctx context.Context, customerID uuid.UUID
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tokens, err := p.legacyTokens(ctx, customerID, balances)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data := &models.GetCustomerWalletResponseData{
|
||||
PointBalance: balances[constants.WalletCurrencyPoint],
|
||||
@@ -94,13 +90,10 @@ func (p *WalletQueryProcessor) Summary(ctx context.Context, customerID uuid.UUID
|
||||
PointDiscountValue: balances[constants.WalletCurrencyPoint] * pointValue,
|
||||
RecentTransactions: recent,
|
||||
TotalPoints: points.TotalPoints,
|
||||
TotalTokens: tokens.TotalTokens,
|
||||
PointsHistory: points.PointsHistory,
|
||||
TokensHistory: tokens.TokensHistory,
|
||||
LastUpdated: points.LastUpdated,
|
||||
}
|
||||
if tokens.LastUpdated.After(data.LastUpdated) {
|
||||
data.LastUpdated = tokens.LastUpdated
|
||||
if len(recent) > 0 {
|
||||
data.LastUpdated = recent[0].CreatedAt
|
||||
}
|
||||
for _, e := range expiring {
|
||||
entry := &models.CustomerWalletExpiring{Amount: e.Amount, Date: e.Date}
|
||||
@@ -161,32 +154,6 @@ func (p *WalletQueryProcessor) legacyPoints(ctx context.Context, customerID uuid
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// LegacyTokens is the pre-wallet GET /customer/tokens. Tokens are EnakCoin now.
|
||||
func (p *WalletQueryProcessor) LegacyTokens(ctx context.Context, customerID uuid.UUID) (*models.GetCustomerTokensResponseData, error) {
|
||||
balances, err := p.repo.SpendableBalances(ctx, customerID, p.now())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return p.legacyTokens(ctx, customerID, balances)
|
||||
}
|
||||
|
||||
func (p *WalletQueryProcessor) legacyTokens(ctx context.Context, customerID uuid.UUID, balances map[string]int64) (*models.GetCustomerTokensResponseData, error) {
|
||||
recent, err := p.recent(ctx, customerID, constants.WalletCurrencyCoin, walletLegacyHistorySize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data := &models.GetCustomerTokensResponseData{TotalTokens: balances[constants.WalletCurrencyCoin]}
|
||||
for _, tx := range recent {
|
||||
data.TokensHistory = append(data.TokensHistory, models.TokensHistoryItem{
|
||||
ID: tx.ID.String(), Tokens: tx.Amount, Type: tx.Type, Description: tx.Description, CreatedAt: tx.CreatedAt,
|
||||
})
|
||||
}
|
||||
if len(recent) > 0 {
|
||||
data.LastUpdated = recent[0].CreatedAt
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (p *WalletQueryProcessor) recent(ctx context.Context, customerID uuid.UUID, currency string, limit int) ([]models.CustomerWalletTransaction, error) {
|
||||
rows, _, err := p.repo.ListTransactions(ctx, repository.WalletTransactionFilter{
|
||||
CustomerID: customerID, Currency: currency, Limit: limit,
|
||||
|
||||
@@ -122,7 +122,6 @@ func TestWalletQueryProcessor_SummaryShowsWhereEachRowCameFromOrWent(t *testing.
|
||||
|
||||
// The pre-wallet fields are still filled, from the wallet.
|
||||
assert.Equal(t, int64(250), data.TotalPoints)
|
||||
assert.Equal(t, int64(8), data.TotalTokens)
|
||||
require.Len(t, data.PointsHistory, 2)
|
||||
assert.Equal(t, int64(-50), data.PointsHistory[0].Points)
|
||||
assert.Equal(t, constants.WalletTxTypePayment, data.PointsHistory[0].Type)
|
||||
@@ -174,12 +173,6 @@ func TestWalletQueryProcessor_LegacyEndpointsSplitByCurrency(t *testing.T) {
|
||||
assert.Equal(t, int64(100), points.TotalPoints)
|
||||
require.Len(t, points.PointsHistory, 1)
|
||||
assert.Equal(t, int64(100), points.PointsHistory[0].Points)
|
||||
|
||||
tokens, err := p.LegacyTokens(context.Background(), uuid.New())
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(8), tokens.TotalTokens)
|
||||
require.Len(t, tokens.TokensHistory, 1)
|
||||
assert.Equal(t, int64(8), tokens.TokensHistory[0].Tokens)
|
||||
}
|
||||
|
||||
func TestWalletQueryProcessor_TransactionsQuery(t *testing.T) {
|
||||
|
||||
@@ -169,7 +169,6 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
|
||||
customer.Use(r.customerAuthMiddleware.ValidateCustomerToken())
|
||||
{
|
||||
customer.GET("/points", r.customerPointsHandler.GetCustomerPoints)
|
||||
customer.GET("/tokens", r.customerPointsHandler.GetCustomerTokens)
|
||||
customer.GET("/wallet", r.customerPointsHandler.GetCustomerWallet)
|
||||
customer.GET("/wallet/transactions", r.customerPointsHandler.GetCustomerWalletTransactions)
|
||||
customer.GET("/wallet/expiring", r.customerPointsHandler.GetCustomerWalletExpiring)
|
||||
|
||||
@@ -177,7 +177,7 @@ func (s *campaignService) CreateCampaignRule(ctx context.Context, req *contract.
|
||||
}
|
||||
|
||||
// Validate reward value based on reward type
|
||||
if req.RewardType == "POINTS" || req.RewardType == "COINS" || req.RewardType == "TOKENS" {
|
||||
if req.RewardType == "POINTS" || req.RewardType == "COINS" {
|
||||
if req.RewardValue == nil || *req.RewardValue <= 0 {
|
||||
return nil, fmt.Errorf("reward value must be positive for %s type", req.RewardType)
|
||||
}
|
||||
@@ -247,7 +247,7 @@ func (s *campaignService) UpdateCampaignRule(ctx context.Context, req *contract.
|
||||
}
|
||||
|
||||
// Validate reward value based on reward type
|
||||
if req.RewardType == "POINTS" || req.RewardType == "COINS" || req.RewardType == "TOKENS" {
|
||||
if req.RewardType == "POINTS" || req.RewardType == "COINS" {
|
||||
if req.RewardValue == nil || *req.RewardValue <= 0 {
|
||||
return nil, fmt.Errorf("reward value must be positive for %s type", req.RewardType)
|
||||
}
|
||||
@@ -295,7 +295,7 @@ func (s *campaignService) GetCampaignRulesByCampaignID(ctx context.Context, camp
|
||||
}
|
||||
|
||||
func (s *campaignService) validateCampaignType(campaignType string) error {
|
||||
validTypes := []string{"REWARD", "POINTS", "COINS", "TOKENS", "MIXED"}
|
||||
validTypes := []string{"REWARD", "POINTS", "COINS", "MIXED"}
|
||||
for _, validType := range validTypes {
|
||||
if campaignType == validType {
|
||||
return nil
|
||||
@@ -335,7 +335,7 @@ func (s *campaignService) validateCampaignRules(rules []contract.CampaignRuleStr
|
||||
}
|
||||
|
||||
// Validate reward value based on reward type
|
||||
if rule.RewardType == "POINTS" || rule.RewardType == "COINS" || rule.RewardType == "TOKENS" {
|
||||
if rule.RewardType == "POINTS" || rule.RewardType == "COINS" {
|
||||
if rule.RewardValue == nil || *rule.RewardValue <= 0 {
|
||||
return fmt.Errorf("reward value must be positive for %s type in rule %d", rule.RewardType, i+1)
|
||||
}
|
||||
@@ -365,7 +365,7 @@ func (s *campaignService) validateRuleType(ruleType string) error {
|
||||
}
|
||||
|
||||
func (s *campaignService) validateRewardType(rewardType string) error {
|
||||
validTypes := []string{"POINTS", "COINS", "TOKENS", "REWARD"}
|
||||
validTypes := []string{"POINTS", "COINS", "REWARD"}
|
||||
for _, validType := range validTypes {
|
||||
if rewardType == validType {
|
||||
return nil
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
|
||||
type CustomerPointsService interface {
|
||||
GetCustomerPoints(ctx context.Context, customerID string) (*models.GetCustomerPointsResponse, error)
|
||||
GetCustomerTokens(ctx context.Context, customerID string) (*models.GetCustomerTokensResponse, error)
|
||||
GetCustomerWallet(ctx context.Context, customerID string) (*models.GetCustomerWalletResponse, error)
|
||||
GetCustomerWalletTransactions(ctx context.Context, customerID string, query models.ListCustomerWalletTransactionsQuery) (*models.PaginatedResponse[models.CustomerWalletTransaction], error)
|
||||
GetCustomerWalletExpiring(ctx context.Context, customerID string) (*models.CustomerWalletExpiringList, error)
|
||||
@@ -41,19 +40,6 @@ func (s *customerPointsService) GetCustomerPoints(ctx context.Context, customerI
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (s *customerPointsService) GetCustomerTokens(ctx context.Context, customerID string) (*models.GetCustomerTokensResponse, error) {
|
||||
if customerID == "" {
|
||||
return nil, fmt.Errorf("customer ID is required")
|
||||
}
|
||||
|
||||
response, err := s.customerPointsProcessor.GetCustomerTotalTokensAPI(ctx, customerID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get customer tokens: %w", err)
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (s *customerPointsService) GetCustomerWallet(ctx context.Context, customerID string) (*models.GetCustomerWalletResponse, error) {
|
||||
if customerID == "" {
|
||||
return nil, fmt.Errorf("customer ID is required")
|
||||
|
||||
@@ -276,7 +276,6 @@ func GamePlayModelToResponse(model *models.GamePlayResponse) *contract.GamePlayR
|
||||
CustomerID: model.CustomerID,
|
||||
PrizeID: model.PrizeID,
|
||||
CoinsUsed: model.CoinsUsed,
|
||||
TokenUsed: model.CoinsUsed,
|
||||
RandomSeed: model.RandomSeed,
|
||||
CreatedAt: model.CreatedAt,
|
||||
Game: game,
|
||||
@@ -306,10 +305,9 @@ func PlayGameModelToResponse(model *models.PlayGameResponse) *contract.PlayGameR
|
||||
}
|
||||
|
||||
return &contract.PlayGameResponse{
|
||||
GamePlay: gamePlayValue,
|
||||
PrizeWon: prizeWon,
|
||||
CoinsRemaining: model.CoinsRemaining,
|
||||
TokensRemaining: model.CoinsRemaining,
|
||||
GamePlay: gamePlayValue,
|
||||
PrizeWon: prizeWon,
|
||||
CoinsRemaining: model.CoinsRemaining,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -254,7 +254,7 @@ func (v *CampaignValidatorImpl) ValidateDeleteCampaignRuleRequest(req *contract.
|
||||
}
|
||||
|
||||
func (v *CampaignValidatorImpl) isValidCampaignType(campaignType string) bool {
|
||||
validTypes := []string{"REWARD", "POINTS", "COINS", "TOKENS", "MIXED"}
|
||||
validTypes := []string{"REWARD", "POINTS", "COINS", "MIXED"}
|
||||
return contains(validTypes, campaignType)
|
||||
}
|
||||
|
||||
@@ -264,7 +264,7 @@ func (v *CampaignValidatorImpl) isValidRuleType(ruleType string) bool {
|
||||
}
|
||||
|
||||
func (v *CampaignValidatorImpl) isValidRewardType(rewardType string) bool {
|
||||
validTypes := []string{"POINTS", "COINS", "TOKENS", "REWARD"}
|
||||
validTypes := []string{"POINTS", "COINS", "REWARD"}
|
||||
return contains(validTypes, rewardType)
|
||||
}
|
||||
|
||||
@@ -297,7 +297,7 @@ func (v *CampaignValidatorImpl) validateCampaignRule(rule *contract.CampaignRule
|
||||
}
|
||||
|
||||
// Validate reward value based on reward type
|
||||
if rule.RewardType == "POINTS" || rule.RewardType == "COINS" || rule.RewardType == "TOKENS" {
|
||||
if rule.RewardType == "POINTS" || rule.RewardType == "COINS" {
|
||||
if rule.RewardValue == nil || *rule.RewardValue <= 0 {
|
||||
return errors.New("reward value must be positive for " + rule.RewardType + " type in rule " + string(rune(ruleNumber)))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user