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
@@ -426,14 +426,11 @@ Semua nilai `data` berupa string. Push hanya sampai ke device yang terdaftar lew
|
|||||||
|
|
||||||
### Endpoint dan field deprecated
|
### Endpoint dan field deprecated
|
||||||
|
|
||||||
Masih jalan dan membaca wallet, tapi akan dihapus setelah semua versi aplikasi pindah.
|
Masih jalan dan membaca wallet, tapi akan dihapus setelah semua versi aplikasi pindah. Semua yang bernama token (`/customer/tokens`, `total_tokens`, `tokens_history`, `token_used`, `tokens_remaining`, campaign `TOKENS`) sudah dihapus; pakai `coin_balance`, `coins_used`, `coins_remaining`, dan `COINS`.
|
||||||
|
|
||||||
| Lama | Pengganti |
|
| Lama | Pengganti |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `GET /customer/points` | `GET /customer/wallet` → `point_balance` |
|
| `GET /customer/points` | `GET /customer/wallet` → `point_balance` |
|
||||||
| `GET /customer/tokens` | `GET /customer/wallet` → `coin_balance` |
|
| `total_points`, `points_history`, `last_updated` di `/customer/wallet` | `point_balance`, `recent_transactions` |
|
||||||
| `total_points`, `total_tokens`, `points_history`, `tokens_history`, `last_updated` di `/customer/wallet` | `point_balance`, `coin_balance`, `recent_transactions` |
|
|
||||||
| `token_used`, `tokens_remaining` di response game | `coins_used`, `coins_remaining` |
|
|
||||||
| `sort_by=token_used` di daftar game play | `sort_by=coins_used` |
|
|
||||||
|
|
||||||
Panduan alur lengkap per tim ada di [`integration-enakpoint.md`](./integration-enakpoint.md).
|
Panduan alur lengkap per tim ada di [`integration-enakpoint.md`](./integration-enakpoint.md).
|
||||||
|
|||||||
@@ -474,16 +474,14 @@ Response:
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"game_play": { "id": "…", "game_id": "…", "coins_used": 1, "token_used": 1, "created_at": "…" },
|
"game_play": { "id": "…", "game_id": "…", "coins_used": 1, "created_at": "…" },
|
||||||
"prize_won": { "id": "…", "name": "Voucher 10rb", … },
|
"prize_won": { "id": "…", "name": "Voucher 10rb", … },
|
||||||
"coins_remaining": 7,
|
"coins_remaining": 7
|
||||||
"tokens_remaining": 7
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
EnakCoin kurang, game nonaktif, atau hadiah baru saja habis dijawab `304`; tidak ada
|
EnakCoin kurang, game nonaktif, atau hadiah baru saja habis dijawab `304`; tidak ada
|
||||||
EnakCoin yang terpotong. Baca `coins_used` dan `coins_remaining`; `token_used` dan
|
EnakCoin yang terpotong.
|
||||||
`tokens_remaining` hanya salinan untuk versi aplikasi lama.
|
|
||||||
|
|
||||||
Di dashboard, `metadata.coin_cost` diisi per game dengan bilangan bulat ≥ 1.
|
Di dashboard, `metadata.coin_cost` diisi per game dengan bilangan bulat ≥ 1.
|
||||||
|
|
||||||
@@ -497,13 +495,14 @@ pindah. Aplikasi baru jangan memakainya.
|
|||||||
| Lama | Ganti dengan |
|
| Lama | Ganti dengan |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `GET /customer/points` | `GET /customer/wallet` (`point_balance`) |
|
| `GET /customer/points` | `GET /customer/wallet` (`point_balance`) |
|
||||||
| `GET /customer/tokens` | `GET /customer/wallet` (`coin_balance`) |
|
| `total_points`, `points_history`, `last_updated` di `/customer/wallet` | `point_balance`, `recent_transactions` |
|
||||||
| `total_points`, `total_tokens`, `points_history`, `tokens_history`, `last_updated` di `/customer/wallet` | `point_balance`, `coin_balance`, `recent_transactions` |
|
|
||||||
| `token_used`, `tokens_remaining` di respons game | `coins_used`, `coins_remaining` |
|
|
||||||
| `sort_by=token_used` di daftar game play | `sort_by=coins_used` |
|
|
||||||
|
|
||||||
Beri tahu tim backend setelah aplikasi yang beredar tidak lagi memakai kolom kiri,
|
Beri tahu tim backend setelah aplikasi yang beredar tidak lagi memakai kolom kiri,
|
||||||
supaya alias dan tabel lama (`customer_points`, `customer_tokens`) bisa dihapus.
|
supaya alias ini bisa dihapus.
|
||||||
|
|
||||||
|
Semua yang bernama token sudah dihapus: `GET /customer/tokens`, `total_tokens`,
|
||||||
|
`tokens_history`, `token_used`, `tokens_remaining`, dan nilai `TOKENS` di campaign. Pakai
|
||||||
|
`coin_balance`, `coins_used`, `coins_remaining`, dan `COINS`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,6 @@ const (
|
|||||||
TableEntity = "table"
|
TableEntity = "table"
|
||||||
// Gamification entities
|
// Gamification entities
|
||||||
CustomerPointsEntity = "customer_points"
|
CustomerPointsEntity = "customer_points"
|
||||||
CustomerTokensEntity = "customer_tokens"
|
|
||||||
TierEntity = "tier"
|
TierEntity = "tier"
|
||||||
GameEntity = "game"
|
GameEntity = "game"
|
||||||
GamePrizeEntity = "game_prize"
|
GamePrizeEntity = "game_prize"
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
type CreateCampaignRequest struct {
|
type CreateCampaignRequest struct {
|
||||||
Name string `json:"name" binding:"required,min=1,max=150"`
|
Name string `json:"name" binding:"required,min=1,max=150"`
|
||||||
Description *string `json:"description,omitempty"`
|
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"`
|
StartDate time.Time `json:"start_date" binding:"required"`
|
||||||
EndDate time.Time `json:"end_date" binding:"required"`
|
EndDate time.Time `json:"end_date" binding:"required"`
|
||||||
IsActive bool `json:"is_active"`
|
IsActive bool `json:"is_active"`
|
||||||
@@ -25,7 +25,7 @@ type UpdateCampaignRequest struct {
|
|||||||
ID uuid.UUID `json:"id" binding:"required"`
|
ID uuid.UUID `json:"id" binding:"required"`
|
||||||
Name string `json:"name" binding:"required,min=1,max=150"`
|
Name string `json:"name" binding:"required,min=1,max=150"`
|
||||||
Description *string `json:"description,omitempty"`
|
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"`
|
StartDate time.Time `json:"start_date" binding:"required"`
|
||||||
EndDate time.Time `json:"end_date" binding:"required"`
|
EndDate time.Time `json:"end_date" binding:"required"`
|
||||||
IsActive bool `json:"is_active"`
|
IsActive bool `json:"is_active"`
|
||||||
@@ -58,7 +58,7 @@ type CreateCampaignRuleRequest struct {
|
|||||||
CampaignID uuid.UUID `json:"campaign_id" binding:"required"`
|
CampaignID uuid.UUID `json:"campaign_id" binding:"required"`
|
||||||
RuleType string `json:"rule_type" binding:"required,oneof=TIER SPEND PRODUCT CATEGORY DAY LOCATION"`
|
RuleType string `json:"rule_type" binding:"required,oneof=TIER SPEND PRODUCT CATEGORY DAY LOCATION"`
|
||||||
ConditionValue *string `json:"condition_value,omitempty"`
|
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"`
|
RewardValue *int64 `json:"reward_value,omitempty"`
|
||||||
RewardSubtype *string `json:"reward_subtype,omitempty"`
|
RewardSubtype *string `json:"reward_subtype,omitempty"`
|
||||||
RewardRefID *uuid.UUID `json:"reward_ref_id,omitempty"`
|
RewardRefID *uuid.UUID `json:"reward_ref_id,omitempty"`
|
||||||
@@ -70,7 +70,7 @@ type UpdateCampaignRuleRequest struct {
|
|||||||
CampaignID uuid.UUID `json:"campaign_id" binding:"required"`
|
CampaignID uuid.UUID `json:"campaign_id" binding:"required"`
|
||||||
RuleType string `json:"rule_type" binding:"required,oneof=TIER SPEND PRODUCT CATEGORY DAY LOCATION"`
|
RuleType string `json:"rule_type" binding:"required,oneof=TIER SPEND PRODUCT CATEGORY DAY LOCATION"`
|
||||||
ConditionValue *string `json:"condition_value,omitempty"`
|
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"`
|
RewardValue *int64 `json:"reward_value,omitempty"`
|
||||||
RewardSubtype *string `json:"reward_subtype,omitempty"`
|
RewardSubtype *string `json:"reward_subtype,omitempty"`
|
||||||
RewardRefID *uuid.UUID `json:"reward_ref_id,omitempty"`
|
RewardRefID *uuid.UUID `json:"reward_ref_id,omitempty"`
|
||||||
@@ -142,7 +142,7 @@ type ListCampaignRulesResponse struct {
|
|||||||
type CampaignRuleStruct struct {
|
type CampaignRuleStruct struct {
|
||||||
RuleType string `json:"rule_type" binding:"required,oneof=TIER SPEND PRODUCT CATEGORY DAY LOCATION"`
|
RuleType string `json:"rule_type" binding:"required,oneof=TIER SPEND PRODUCT CATEGORY DAY LOCATION"`
|
||||||
ConditionValue *string `json:"condition_value,omitempty"`
|
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"`
|
RewardValue *int64 `json:"reward_value,omitempty"`
|
||||||
RewardSubtype *string `json:"reward_subtype,omitempty"`
|
RewardSubtype *string `json:"reward_subtype,omitempty"`
|
||||||
RewardRefID *uuid.UUID `json:"reward_ref_id,omitempty"`
|
RewardRefID *uuid.UUID `json:"reward_ref_id,omitempty"`
|
||||||
|
|||||||
@@ -25,26 +25,6 @@ type PointsHistoryItem struct {
|
|||||||
CreatedAt time.Time `json:"created_at"`
|
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 {
|
type GetCustomerWalletResponse struct {
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
@@ -53,8 +33,6 @@ type GetCustomerWalletResponse struct {
|
|||||||
|
|
||||||
type GetCustomerWalletResponseData struct {
|
type GetCustomerWalletResponseData struct {
|
||||||
TotalPoints int64 `json:"total_points"`
|
TotalPoints int64 `json:"total_points"`
|
||||||
TotalTokens int64 `json:"total_tokens"`
|
|
||||||
PointsHistory []PointsHistoryItem `json:"points_history,omitempty"`
|
PointsHistory []PointsHistoryItem `json:"points_history,omitempty"`
|
||||||
TokensHistory []TokensHistoryItem `json:"tokens_history,omitempty"`
|
|
||||||
LastUpdated time.Time `json:"last_updated"`
|
LastUpdated time.Time `json:"last_updated"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,13 +14,11 @@ type CreateGamePlayRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type GamePlayResponse struct {
|
type GamePlayResponse struct {
|
||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
GameID uuid.UUID `json:"game_id"`
|
GameID uuid.UUID `json:"game_id"`
|
||||||
CustomerID uuid.UUID `json:"customer_id"`
|
CustomerID uuid.UUID `json:"customer_id"`
|
||||||
PrizeID *uuid.UUID `json:"prize_id,omitempty"`
|
PrizeID *uuid.UUID `json:"prize_id,omitempty"`
|
||||||
CoinsUsed int `json:"coins_used"`
|
CoinsUsed int `json:"coins_used"`
|
||||||
// Deprecated: the same as CoinsUsed, kept until the apps read coins_used.
|
|
||||||
TokenUsed int `json:"token_used"`
|
|
||||||
RandomSeed *string `json:"random_seed,omitempty"`
|
RandomSeed *string `json:"random_seed,omitempty"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
Game *GameResponse `json:"game,omitempty"`
|
Game *GameResponse `json:"game,omitempty"`
|
||||||
@@ -35,7 +33,7 @@ type ListGamePlaysRequest struct {
|
|||||||
GameID *uuid.UUID `json:"game_id"`
|
GameID *uuid.UUID `json:"game_id"`
|
||||||
CustomerID *uuid.UUID `json:"customer_id"`
|
CustomerID *uuid.UUID `json:"customer_id"`
|
||||||
PrizeID *uuid.UUID `json:"prize_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"`
|
SortOrder string `json:"sort_order" validate:"omitempty,oneof=asc desc"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,6 +56,4 @@ type PlayGameResponse struct {
|
|||||||
GamePlay GamePlayResponse `json:"game_play"`
|
GamePlay GamePlayResponse `json:"game_play"`
|
||||||
PrizeWon *GamePrizeResponse `json:"prize_won,omitempty"`
|
PrizeWon *GamePrizeResponse `json:"prize_won,omitempty"`
|
||||||
CoinsRemaining int64 `json:"coins_remaining"`
|
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"`
|
GamePlay GamePlayResponse `json:"game_play"`
|
||||||
PrizeWon *CustomerGamePrizeResponse `json:"prize_won,omitempty"`
|
PrizeWon *CustomerGamePrizeResponse `json:"prize_won,omitempty"`
|
||||||
CoinsRemaining int64 `json:"coins_remaining"`
|
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"
|
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
|
type RuleType string
|
||||||
|
|
||||||
const (
|
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")
|
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) {
|
func (h *CustomerPointsHandler) GetCustomerWallet(c *gin.Context) {
|
||||||
ctx := c.Request.Context()
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Drives the customer wallet endpoints over HTTP down to Postgres, to show the app
|
// 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
|
// and /wallet shapes. Needs TEST_DATABASE_URL pointing at a migrated database; see
|
||||||
// internal/repository/wallet_repository_test.go.
|
// internal/repository/wallet_repository_test.go.
|
||||||
func TestCustomerWalletEndpoints_AgainstPostgres(t *testing.T) {
|
func TestCustomerWalletEndpoints_AgainstPostgres(t *testing.T) {
|
||||||
@@ -111,7 +111,6 @@ func TestCustomerWalletEndpoints_AgainstPostgres(t *testing.T) {
|
|||||||
}{{"/a", customer}, {"/b", other}, {"/gone", uuid.New()}} {
|
}{{"/a", customer}, {"/b", other}, {"/gone", uuid.New()}} {
|
||||||
g := router.Group(who.prefix, as(who.id))
|
g := router.Group(who.prefix, as(who.id))
|
||||||
g.GET("/points", h.GetCustomerPoints)
|
g.GET("/points", h.GetCustomerPoints)
|
||||||
g.GET("/tokens", h.GetCustomerTokens)
|
|
||||||
g.GET("/wallet", h.GetCustomerWallet)
|
g.GET("/wallet", h.GetCustomerWallet)
|
||||||
g.GET("/wallet/transactions", h.GetCustomerWalletTransactions)
|
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)
|
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")
|
status, body := get("/a/points")
|
||||||
require.Equal(t, http.StatusOK, status, body)
|
require.Equal(t, http.StatusOK, status, body)
|
||||||
assert.EqualValues(t, 250, inner(body)["total_points"], "300 - 50; the expired 20 is not spendable")
|
assert.EqualValues(t, 250, inner(body)["total_points"], "300 - 50; the expired 20 is not spendable")
|
||||||
assert.Len(t, inner(body)["points_history"], 3)
|
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.
|
// /wallet: new fields, with the old ones still there.
|
||||||
status, body = get("/a/wallet")
|
status, body = get("/a/wallet")
|
||||||
require.Equal(t, http.StatusOK, status, body)
|
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")},
|
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")
|
nearest["point"], "both lots fall on 2 July in Jakarta although they are on different UTC days")
|
||||||
assert.Nil(t, nearest["coin"])
|
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.Contains(t, w, legacy)
|
||||||
}
|
}
|
||||||
|
assert.NotContains(t, w, "total_tokens", "tokens are gone; EnakCoin is coin_balance")
|
||||||
assert.EqualValues(t, 250, w["total_points"])
|
assert.EqualValues(t, 250, w["total_points"])
|
||||||
|
|
||||||
recent := w["recent_transactions"].([]any)
|
recent := w["recent_transactions"].([]any)
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ func ToCampaignEntity(request *contract.CreateCampaignRequest) *entities.Campaig
|
|||||||
return &entities.Campaign{
|
return &entities.Campaign{
|
||||||
Name: request.Name,
|
Name: request.Name,
|
||||||
Description: request.Description,
|
Description: request.Description,
|
||||||
Type: entities.NormalizeCampaignType(request.Type),
|
Type: entities.CampaignType(request.Type),
|
||||||
StartDate: request.StartDate,
|
StartDate: request.StartDate,
|
||||||
EndDate: request.EndDate,
|
EndDate: request.EndDate,
|
||||||
IsActive: request.IsActive,
|
IsActive: request.IsActive,
|
||||||
@@ -91,7 +91,7 @@ func ToCampaignEntityFromUpdate(request *contract.UpdateCampaignRequest) *entiti
|
|||||||
ID: request.ID,
|
ID: request.ID,
|
||||||
Name: request.Name,
|
Name: request.Name,
|
||||||
Description: request.Description,
|
Description: request.Description,
|
||||||
Type: entities.NormalizeCampaignType(request.Type),
|
Type: entities.CampaignType(request.Type),
|
||||||
StartDate: request.StartDate,
|
StartDate: request.StartDate,
|
||||||
EndDate: request.EndDate,
|
EndDate: request.EndDate,
|
||||||
IsActive: request.IsActive,
|
IsActive: request.IsActive,
|
||||||
@@ -118,7 +118,7 @@ func ToCampaignRuleEntity(request *contract.CampaignRuleStruct, campaignID uuid.
|
|||||||
CampaignID: campaignID,
|
CampaignID: campaignID,
|
||||||
RuleType: entities.RuleType(request.RuleType),
|
RuleType: entities.RuleType(request.RuleType),
|
||||||
ConditionValue: request.ConditionValue,
|
ConditionValue: request.ConditionValue,
|
||||||
RewardType: entities.NormalizeCampaignRewardType(request.RewardType),
|
RewardType: entities.CampaignRewardType(request.RewardType),
|
||||||
RewardValue: request.RewardValue,
|
RewardValue: request.RewardValue,
|
||||||
RewardSubtype: rewardSubtype,
|
RewardSubtype: rewardSubtype,
|
||||||
RewardRefID: request.RewardRefID,
|
RewardRefID: request.RewardRefID,
|
||||||
@@ -142,7 +142,7 @@ func ToCampaignRuleEntityFromUpdate(request *contract.CampaignRuleStruct, campai
|
|||||||
CampaignID: campaignID,
|
CampaignID: campaignID,
|
||||||
RuleType: entities.RuleType(request.RuleType),
|
RuleType: entities.RuleType(request.RuleType),
|
||||||
ConditionValue: request.ConditionValue,
|
ConditionValue: request.ConditionValue,
|
||||||
RewardType: entities.NormalizeCampaignRewardType(request.RewardType),
|
RewardType: entities.CampaignRewardType(request.RewardType),
|
||||||
RewardValue: request.RewardValue,
|
RewardValue: request.RewardValue,
|
||||||
RewardSubtype: rewardSubtype,
|
RewardSubtype: rewardSubtype,
|
||||||
RewardRefID: request.RewardRefID,
|
RewardRefID: request.RewardRefID,
|
||||||
|
|||||||
@@ -36,10 +36,9 @@ func SpinGameResponseDataModelToContract(data *models.SpinGameResponseData) *con
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &contract.SpinGameResponseData{
|
return &contract.SpinGameResponseData{
|
||||||
GamePlay: GamePlayResponseModelToContract(&data.GamePlay),
|
GamePlay: GamePlayResponseModelToContract(&data.GamePlay),
|
||||||
PrizeWon: CustomerGamePrizeResponseModelToContract(data.PrizeWon),
|
PrizeWon: CustomerGamePrizeResponseModelToContract(data.PrizeWon),
|
||||||
CoinsRemaining: data.CoinsRemaining,
|
CoinsRemaining: data.CoinsRemaining,
|
||||||
TokensRemaining: data.CoinsRemaining,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,7 +54,6 @@ func GamePlayResponseModelToContract(resp *models.GamePlayResponse) contract.Gam
|
|||||||
CustomerID: resp.CustomerID,
|
CustomerID: resp.CustomerID,
|
||||||
PrizeID: resp.PrizeID,
|
PrizeID: resp.PrizeID,
|
||||||
CoinsUsed: resp.CoinsUsed,
|
CoinsUsed: resp.CoinsUsed,
|
||||||
TokenUsed: resp.CoinsUsed,
|
|
||||||
RandomSeed: resp.RandomSeed,
|
RandomSeed: resp.RandomSeed,
|
||||||
CreatedAt: resp.CreatedAt,
|
CreatedAt: resp.CreatedAt,
|
||||||
Game: nil, // Optional field - can be populated separately if needed
|
Game: nil, // Optional field - can be populated separately if needed
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
type CreateCampaignRequest struct {
|
type CreateCampaignRequest struct {
|
||||||
Name string `json:"name" binding:"required,min=1,max=150"`
|
Name string `json:"name" binding:"required,min=1,max=150"`
|
||||||
Description *string `json:"description,omitempty"`
|
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"`
|
StartDate time.Time `json:"start_date" binding:"required"`
|
||||||
EndDate time.Time `json:"end_date" binding:"required"`
|
EndDate time.Time `json:"end_date" binding:"required"`
|
||||||
IsActive bool `json:"is_active"`
|
IsActive bool `json:"is_active"`
|
||||||
@@ -25,7 +25,7 @@ type UpdateCampaignRequest struct {
|
|||||||
ID uuid.UUID `json:"id" binding:"required"`
|
ID uuid.UUID `json:"id" binding:"required"`
|
||||||
Name string `json:"name" binding:"required,min=1,max=150"`
|
Name string `json:"name" binding:"required,min=1,max=150"`
|
||||||
Description *string `json:"description,omitempty"`
|
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"`
|
StartDate time.Time `json:"start_date" binding:"required"`
|
||||||
EndDate time.Time `json:"end_date" binding:"required"`
|
EndDate time.Time `json:"end_date" binding:"required"`
|
||||||
IsActive bool `json:"is_active"`
|
IsActive bool `json:"is_active"`
|
||||||
@@ -88,7 +88,7 @@ type CreateCampaignRuleRequest struct {
|
|||||||
CampaignID uuid.UUID `json:"campaign_id" binding:"required"`
|
CampaignID uuid.UUID `json:"campaign_id" binding:"required"`
|
||||||
RuleType string `json:"rule_type" binding:"required,oneof=TIER SPEND PRODUCT CATEGORY DAY LOCATION"`
|
RuleType string `json:"rule_type" binding:"required,oneof=TIER SPEND PRODUCT CATEGORY DAY LOCATION"`
|
||||||
ConditionValue *string `json:"condition_value,omitempty"`
|
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"`
|
RewardValue *int64 `json:"reward_value,omitempty"`
|
||||||
RewardSubtype *string `json:"reward_subtype,omitempty"`
|
RewardSubtype *string `json:"reward_subtype,omitempty"`
|
||||||
RewardRefID *uuid.UUID `json:"reward_ref_id,omitempty"`
|
RewardRefID *uuid.UUID `json:"reward_ref_id,omitempty"`
|
||||||
@@ -100,7 +100,7 @@ type UpdateCampaignRuleRequest struct {
|
|||||||
CampaignID uuid.UUID `json:"campaign_id" binding:"required"`
|
CampaignID uuid.UUID `json:"campaign_id" binding:"required"`
|
||||||
RuleType string `json:"rule_type" binding:"required,oneof=TIER SPEND PRODUCT CATEGORY DAY LOCATION"`
|
RuleType string `json:"rule_type" binding:"required,oneof=TIER SPEND PRODUCT CATEGORY DAY LOCATION"`
|
||||||
ConditionValue *string `json:"condition_value,omitempty"`
|
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"`
|
RewardValue *int64 `json:"reward_value,omitempty"`
|
||||||
RewardSubtype *string `json:"reward_subtype,omitempty"`
|
RewardSubtype *string `json:"reward_subtype,omitempty"`
|
||||||
RewardRefID *uuid.UUID `json:"reward_ref_id,omitempty"`
|
RewardRefID *uuid.UUID `json:"reward_ref_id,omitempty"`
|
||||||
@@ -126,7 +126,7 @@ type ListCampaignRulesResponse struct {
|
|||||||
type CampaignRuleStruct struct {
|
type CampaignRuleStruct struct {
|
||||||
RuleType string `json:"rule_type" binding:"required,oneof=TIER SPEND PRODUCT CATEGORY DAY LOCATION"`
|
RuleType string `json:"rule_type" binding:"required,oneof=TIER SPEND PRODUCT CATEGORY DAY LOCATION"`
|
||||||
ConditionValue *string `json:"condition_value,omitempty"`
|
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"`
|
RewardValue *int64 `json:"reward_value,omitempty"`
|
||||||
RewardSubtype *string `json:"reward_subtype,omitempty"`
|
RewardSubtype *string `json:"reward_subtype,omitempty"`
|
||||||
RewardRefID *uuid.UUID `json:"reward_ref_id,omitempty"`
|
RewardRefID *uuid.UUID `json:"reward_ref_id,omitempty"`
|
||||||
|
|||||||
@@ -25,26 +25,6 @@ type PointsHistoryItem struct {
|
|||||||
CreatedAt time.Time `json:"created_at"`
|
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 {
|
type GetCustomerWalletResponse struct {
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
@@ -66,8 +46,6 @@ type GetCustomerWalletResponseData struct {
|
|||||||
// Deprecated: the shape of this endpoint before the wallet, kept for app versions
|
// 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.
|
// that still read it. Filled from the wallet like the fields above.
|
||||||
TotalPoints int64 `json:"total_points"`
|
TotalPoints int64 `json:"total_points"`
|
||||||
TotalTokens int64 `json:"total_tokens"`
|
|
||||||
PointsHistory []PointsHistoryItem `json:"points_history,omitempty"`
|
PointsHistory []PointsHistoryItem `json:"points_history,omitempty"`
|
||||||
TokensHistory []TokensHistoryItem `json:"tokens_history,omitempty"`
|
|
||||||
LastUpdated time.Time `json:"last_updated"`
|
LastUpdated time.Time `json:"last_updated"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ type ListGamePlaysQuery struct {
|
|||||||
GameID *uuid.UUID `query:"game_id"`
|
GameID *uuid.UUID `query:"game_id"`
|
||||||
CustomerID *uuid.UUID `query:"customer_id"`
|
CustomerID *uuid.UUID `query:"customer_id"`
|
||||||
PrizeID *uuid.UUID `query:"prize_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"`
|
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,
|
Page: req.Page,
|
||||||
Limit: req.Limit,
|
Limit: req.Limit,
|
||||||
Search: req.Search,
|
Search: req.Search,
|
||||||
Type: string(entities.NormalizeCampaignType(req.Type)),
|
Type: req.Type,
|
||||||
IsActive: req.IsActive,
|
IsActive: req.IsActive,
|
||||||
ShowOnApp: req.ShowOnApp,
|
ShowOnApp: req.ShowOnApp,
|
||||||
StartDate: req.StartDate,
|
StartDate: req.StartDate,
|
||||||
@@ -178,7 +178,7 @@ func (p *campaignRuleProcessor) CreateCampaignRule(ctx context.Context, req *con
|
|||||||
CampaignID: req.CampaignID,
|
CampaignID: req.CampaignID,
|
||||||
RuleType: entities.RuleType(req.RuleType),
|
RuleType: entities.RuleType(req.RuleType),
|
||||||
ConditionValue: req.ConditionValue,
|
ConditionValue: req.ConditionValue,
|
||||||
RewardType: entities.NormalizeCampaignRewardType(req.RewardType),
|
RewardType: entities.CampaignRewardType(req.RewardType),
|
||||||
RewardValue: req.RewardValue,
|
RewardValue: req.RewardValue,
|
||||||
RewardSubtype: (*entities.RewardSubtype)(req.RewardSubtype),
|
RewardSubtype: (*entities.RewardSubtype)(req.RewardSubtype),
|
||||||
RewardRefID: req.RewardRefID,
|
RewardRefID: req.RewardRefID,
|
||||||
@@ -218,7 +218,7 @@ func (p *campaignRuleProcessor) ListCampaignRules(ctx context.Context, req *cont
|
|||||||
Limit: req.Limit,
|
Limit: req.Limit,
|
||||||
CampaignID: req.CampaignID,
|
CampaignID: req.CampaignID,
|
||||||
RuleType: req.RuleType,
|
RuleType: req.RuleType,
|
||||||
RewardType: string(entities.NormalizeCampaignRewardType(req.RewardType)),
|
RewardType: req.RewardType,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get from repository
|
// Get from repository
|
||||||
@@ -247,7 +247,7 @@ func (p *campaignRuleProcessor) UpdateCampaignRule(ctx context.Context, req *con
|
|||||||
CampaignID: req.CampaignID,
|
CampaignID: req.CampaignID,
|
||||||
RuleType: entities.RuleType(req.RuleType),
|
RuleType: entities.RuleType(req.RuleType),
|
||||||
ConditionValue: req.ConditionValue,
|
ConditionValue: req.ConditionValue,
|
||||||
RewardType: entities.NormalizeCampaignRewardType(req.RewardType),
|
RewardType: entities.CampaignRewardType(req.RewardType),
|
||||||
RewardValue: req.RewardValue,
|
RewardValue: req.RewardValue,
|
||||||
RewardSubtype: (*entities.RewardSubtype)(req.RewardSubtype),
|
RewardSubtype: (*entities.RewardSubtype)(req.RewardSubtype),
|
||||||
RewardRefID: req.RewardRefID,
|
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
|
// EnakPoint & EnakCoin wallet; customer_points and customer_tokens are no longer used
|
||||||
// for balances (docs/prd-point-coin.md §9).
|
// for balances (docs/prd-point-coin.md §9).
|
||||||
|
|
||||||
@@ -42,22 +42,6 @@ func (p *CustomerPointsProcessor) GetCustomerTotalPointsAPI(ctx context.Context,
|
|||||||
}, nil
|
}, 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) {
|
func (p *CustomerPointsProcessor) GetCustomerWalletAPI(ctx context.Context, customerID string) (*models.GetCustomerWalletResponse, error) {
|
||||||
id, err := parseWalletCustomerID(customerID)
|
id, err := parseWalletCustomerID(customerID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -99,10 +99,6 @@ func (p *GamePlayProcessor) ListGamePlays(ctx context.Context, query *models.Lis
|
|||||||
}
|
}
|
||||||
|
|
||||||
offset := (query.Page - 1) * query.Limit
|
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
|
// Get game plays from repository
|
||||||
gamePlays, total, err := p.gamePlayRepo.List(
|
gamePlays, total, err := p.gamePlayRepo.List(
|
||||||
|
|||||||
@@ -82,10 +82,6 @@ func (p *WalletQueryProcessor) Summary(ctx context.Context, customerID uuid.UUID
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
tokens, err := p.legacyTokens(ctx, customerID, balances)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
data := &models.GetCustomerWalletResponseData{
|
data := &models.GetCustomerWalletResponseData{
|
||||||
PointBalance: balances[constants.WalletCurrencyPoint],
|
PointBalance: balances[constants.WalletCurrencyPoint],
|
||||||
@@ -94,13 +90,10 @@ func (p *WalletQueryProcessor) Summary(ctx context.Context, customerID uuid.UUID
|
|||||||
PointDiscountValue: balances[constants.WalletCurrencyPoint] * pointValue,
|
PointDiscountValue: balances[constants.WalletCurrencyPoint] * pointValue,
|
||||||
RecentTransactions: recent,
|
RecentTransactions: recent,
|
||||||
TotalPoints: points.TotalPoints,
|
TotalPoints: points.TotalPoints,
|
||||||
TotalTokens: tokens.TotalTokens,
|
|
||||||
PointsHistory: points.PointsHistory,
|
PointsHistory: points.PointsHistory,
|
||||||
TokensHistory: tokens.TokensHistory,
|
|
||||||
LastUpdated: points.LastUpdated,
|
|
||||||
}
|
}
|
||||||
if tokens.LastUpdated.After(data.LastUpdated) {
|
if len(recent) > 0 {
|
||||||
data.LastUpdated = tokens.LastUpdated
|
data.LastUpdated = recent[0].CreatedAt
|
||||||
}
|
}
|
||||||
for _, e := range expiring {
|
for _, e := range expiring {
|
||||||
entry := &models.CustomerWalletExpiring{Amount: e.Amount, Date: e.Date}
|
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
|
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) {
|
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{
|
rows, _, err := p.repo.ListTransactions(ctx, repository.WalletTransactionFilter{
|
||||||
CustomerID: customerID, Currency: currency, Limit: limit,
|
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.
|
// The pre-wallet fields are still filled, from the wallet.
|
||||||
assert.Equal(t, int64(250), data.TotalPoints)
|
assert.Equal(t, int64(250), data.TotalPoints)
|
||||||
assert.Equal(t, int64(8), data.TotalTokens)
|
|
||||||
require.Len(t, data.PointsHistory, 2)
|
require.Len(t, data.PointsHistory, 2)
|
||||||
assert.Equal(t, int64(-50), data.PointsHistory[0].Points)
|
assert.Equal(t, int64(-50), data.PointsHistory[0].Points)
|
||||||
assert.Equal(t, constants.WalletTxTypePayment, data.PointsHistory[0].Type)
|
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)
|
assert.Equal(t, int64(100), points.TotalPoints)
|
||||||
require.Len(t, points.PointsHistory, 1)
|
require.Len(t, points.PointsHistory, 1)
|
||||||
assert.Equal(t, int64(100), points.PointsHistory[0].Points)
|
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) {
|
func TestWalletQueryProcessor_TransactionsQuery(t *testing.T) {
|
||||||
|
|||||||
@@ -169,7 +169,6 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
|
|||||||
customer.Use(r.customerAuthMiddleware.ValidateCustomerToken())
|
customer.Use(r.customerAuthMiddleware.ValidateCustomerToken())
|
||||||
{
|
{
|
||||||
customer.GET("/points", r.customerPointsHandler.GetCustomerPoints)
|
customer.GET("/points", r.customerPointsHandler.GetCustomerPoints)
|
||||||
customer.GET("/tokens", r.customerPointsHandler.GetCustomerTokens)
|
|
||||||
customer.GET("/wallet", r.customerPointsHandler.GetCustomerWallet)
|
customer.GET("/wallet", r.customerPointsHandler.GetCustomerWallet)
|
||||||
customer.GET("/wallet/transactions", r.customerPointsHandler.GetCustomerWalletTransactions)
|
customer.GET("/wallet/transactions", r.customerPointsHandler.GetCustomerWalletTransactions)
|
||||||
customer.GET("/wallet/expiring", r.customerPointsHandler.GetCustomerWalletExpiring)
|
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
|
// 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 {
|
if req.RewardValue == nil || *req.RewardValue <= 0 {
|
||||||
return nil, fmt.Errorf("reward value must be positive for %s type", req.RewardType)
|
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
|
// 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 {
|
if req.RewardValue == nil || *req.RewardValue <= 0 {
|
||||||
return nil, fmt.Errorf("reward value must be positive for %s type", req.RewardType)
|
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 {
|
func (s *campaignService) validateCampaignType(campaignType string) error {
|
||||||
validTypes := []string{"REWARD", "POINTS", "COINS", "TOKENS", "MIXED"}
|
validTypes := []string{"REWARD", "POINTS", "COINS", "MIXED"}
|
||||||
for _, validType := range validTypes {
|
for _, validType := range validTypes {
|
||||||
if campaignType == validType {
|
if campaignType == validType {
|
||||||
return nil
|
return nil
|
||||||
@@ -335,7 +335,7 @@ func (s *campaignService) validateCampaignRules(rules []contract.CampaignRuleStr
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate reward value based on reward type
|
// 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 {
|
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)
|
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 {
|
func (s *campaignService) validateRewardType(rewardType string) error {
|
||||||
validTypes := []string{"POINTS", "COINS", "TOKENS", "REWARD"}
|
validTypes := []string{"POINTS", "COINS", "REWARD"}
|
||||||
for _, validType := range validTypes {
|
for _, validType := range validTypes {
|
||||||
if rewardType == validType {
|
if rewardType == validType {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
|
|
||||||
type CustomerPointsService interface {
|
type CustomerPointsService interface {
|
||||||
GetCustomerPoints(ctx context.Context, customerID string) (*models.GetCustomerPointsResponse, error)
|
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)
|
GetCustomerWallet(ctx context.Context, customerID string) (*models.GetCustomerWalletResponse, error)
|
||||||
GetCustomerWalletTransactions(ctx context.Context, customerID string, query models.ListCustomerWalletTransactionsQuery) (*models.PaginatedResponse[models.CustomerWalletTransaction], error)
|
GetCustomerWalletTransactions(ctx context.Context, customerID string, query models.ListCustomerWalletTransactionsQuery) (*models.PaginatedResponse[models.CustomerWalletTransaction], error)
|
||||||
GetCustomerWalletExpiring(ctx context.Context, customerID string) (*models.CustomerWalletExpiringList, 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
|
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) {
|
func (s *customerPointsService) GetCustomerWallet(ctx context.Context, customerID string) (*models.GetCustomerWalletResponse, error) {
|
||||||
if customerID == "" {
|
if customerID == "" {
|
||||||
return nil, fmt.Errorf("customer ID is required")
|
return nil, fmt.Errorf("customer ID is required")
|
||||||
|
|||||||
@@ -276,7 +276,6 @@ func GamePlayModelToResponse(model *models.GamePlayResponse) *contract.GamePlayR
|
|||||||
CustomerID: model.CustomerID,
|
CustomerID: model.CustomerID,
|
||||||
PrizeID: model.PrizeID,
|
PrizeID: model.PrizeID,
|
||||||
CoinsUsed: model.CoinsUsed,
|
CoinsUsed: model.CoinsUsed,
|
||||||
TokenUsed: model.CoinsUsed,
|
|
||||||
RandomSeed: model.RandomSeed,
|
RandomSeed: model.RandomSeed,
|
||||||
CreatedAt: model.CreatedAt,
|
CreatedAt: model.CreatedAt,
|
||||||
Game: game,
|
Game: game,
|
||||||
@@ -306,10 +305,9 @@ func PlayGameModelToResponse(model *models.PlayGameResponse) *contract.PlayGameR
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &contract.PlayGameResponse{
|
return &contract.PlayGameResponse{
|
||||||
GamePlay: gamePlayValue,
|
GamePlay: gamePlayValue,
|
||||||
PrizeWon: prizeWon,
|
PrizeWon: prizeWon,
|
||||||
CoinsRemaining: model.CoinsRemaining,
|
CoinsRemaining: model.CoinsRemaining,
|
||||||
TokensRemaining: model.CoinsRemaining,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -254,7 +254,7 @@ func (v *CampaignValidatorImpl) ValidateDeleteCampaignRuleRequest(req *contract.
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (v *CampaignValidatorImpl) isValidCampaignType(campaignType string) bool {
|
func (v *CampaignValidatorImpl) isValidCampaignType(campaignType string) bool {
|
||||||
validTypes := []string{"REWARD", "POINTS", "COINS", "TOKENS", "MIXED"}
|
validTypes := []string{"REWARD", "POINTS", "COINS", "MIXED"}
|
||||||
return contains(validTypes, campaignType)
|
return contains(validTypes, campaignType)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,7 +264,7 @@ func (v *CampaignValidatorImpl) isValidRuleType(ruleType string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (v *CampaignValidatorImpl) isValidRewardType(rewardType string) bool {
|
func (v *CampaignValidatorImpl) isValidRewardType(rewardType string) bool {
|
||||||
validTypes := []string{"POINTS", "COINS", "TOKENS", "REWARD"}
|
validTypes := []string{"POINTS", "COINS", "REWARD"}
|
||||||
return contains(validTypes, rewardType)
|
return contains(validTypes, rewardType)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -297,7 +297,7 @@ func (v *CampaignValidatorImpl) validateCampaignRule(rule *contract.CampaignRule
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate reward value based on reward type
|
// 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 {
|
if rule.RewardValue == nil || *rule.RewardValue <= 0 {
|
||||||
return errors.New("reward value must be positive for " + rule.RewardType + " type in rule " + string(rune(ruleNumber)))
|
return errors.New("reward value must be positive for " + rule.RewardType + " type in rule " + string(rune(ruleNumber)))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user