Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
100e006218 | ||
|
|
609d434976 | ||
|
|
35fe5703fd | ||
|
|
e200923b72 | ||
|
|
c988a79d3b | ||
|
|
9b21af8892 | ||
|
|
582dc75543 | ||
|
|
c6062c4bb9 | ||
|
|
5e28b05770 | ||
|
|
4e24f9bbb0 | ||
|
|
645da3048e |
+1
-1
@@ -15,6 +15,6 @@ COPY --from=build /out/app /app
|
||||
ENV TZ=Asia/Jakarta
|
||||
EXPOSE 4000
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
|
||||
CMD curl -fsS http://localhost:3300/health || exit 1
|
||||
CMD curl -fsS http://localhost:4000/health || exit 1
|
||||
USER appuser
|
||||
ENTRYPOINT ["/app"]
|
||||
|
||||
+142
-27
@@ -1,62 +1,177 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# Usage:
|
||||
# ./deployment.sh staging deploy staging (branch staging, port 4001)
|
||||
# ./deployment.sh production deploy production (branch main, port 4000)
|
||||
# ./deployment.sh production --rollback kembali ke image sebelum deploy terakhir
|
||||
# Tambahkan --yes untuk melewati konfirmasi production.
|
||||
|
||||
APP_NAME="apskel-pos"
|
||||
LEGACY_CONTAINER="apskel-pos" # nama lama sebelum nama container memakai environment
|
||||
|
||||
# ─── Deteksi environment dari branch aktif ───────────────────────────────────
|
||||
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
||||
usage() {
|
||||
echo "Usage: $0 <staging|production> [--rollback] [--yes]"
|
||||
exit 1
|
||||
}
|
||||
|
||||
case "$CURRENT_BRANCH" in
|
||||
main)
|
||||
ENV_MODE="production"
|
||||
fail() {
|
||||
echo "❌ $*"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# ─── Argumen ──────────────────────────────────────────────────────────────────
|
||||
ENV_MODE="${1:-}"
|
||||
shift || true
|
||||
|
||||
ROLLBACK=false
|
||||
ASSUME_YES=false
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--rollback) ROLLBACK=true ;;
|
||||
--yes|-y) ASSUME_YES=true ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Environment wajib disebut eksplisit, tidak lagi ditebak dari branch aktif
|
||||
case "$ENV_MODE" in
|
||||
production)
|
||||
EXPECTED_BRANCH="main"
|
||||
PORT="4000"
|
||||
;;
|
||||
staging)
|
||||
ENV_MODE="staging"
|
||||
EXPECTED_BRANCH="staging"
|
||||
PORT="4001"
|
||||
;;
|
||||
*)
|
||||
echo "❌ Branch '$CURRENT_BRANCH' tidak dikenali untuk deployment."
|
||||
echo " Gunakan branch 'main' (production) atau 'staging' (staging)."
|
||||
exit 1
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
|
||||
CONTAINER_NAME="$APP_NAME"
|
||||
CONTAINER_NAME="$APP_NAME-$ENV_MODE"
|
||||
IMAGE_NAME="$APP_NAME:$ENV_MODE"
|
||||
PREVIOUS_IMAGE="$APP_NAME:$ENV_MODE-previous"
|
||||
CONFIG_FILE="infra/$ENV_MODE.yaml"
|
||||
|
||||
# ─── Validasi repo ────────────────────────────────────────────────────────────
|
||||
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
||||
|
||||
if [ "$CURRENT_BRANCH" = "HEAD" ]; then
|
||||
fail "Repo sedang detached HEAD. Jalankan 'git checkout $EXPECTED_BRANCH' dulu."
|
||||
fi
|
||||
|
||||
if [ "$CURRENT_BRANCH" != "$EXPECTED_BRANCH" ]; then
|
||||
fail "Deploy $ENV_MODE harus dari branch '$EXPECTED_BRANCH', folder ini di branch '$CURRENT_BRANCH'.
|
||||
Pakai folder terpisah per environment, jangan pindah branch di folder yang sedang dipakai container lain."
|
||||
fi
|
||||
|
||||
if [ -n "$(git status --porcelain --untracked-files=no)" ]; then
|
||||
fail "Ada perubahan yang belum di-commit di folder ini. Bersihkan dulu sebelum deploy."
|
||||
fi
|
||||
|
||||
[ -f "$CONFIG_FILE" ] || fail "Config file '$CONFIG_FILE' tidak ditemukan."
|
||||
|
||||
# ─── Pastikan port tidak dipakai container environment lain ──────────────────
|
||||
OTHER_ON_PORT=$(docker ps --filter "publish=$PORT" --format '{{.Names}}' \
|
||||
| grep -vx "$CONTAINER_NAME" \
|
||||
| { if [ "$ENV_MODE" = "production" ]; then grep -vx "$LEGACY_CONTAINER"; else cat; fi; } \
|
||||
|| true)
|
||||
if [ -n "$OTHER_ON_PORT" ]; then
|
||||
fail "Port $PORT sedang dipakai container lain: $OTHER_ON_PORT"
|
||||
fi
|
||||
|
||||
echo "📦 Environment : $ENV_MODE"
|
||||
echo "🌿 Branch : $CURRENT_BRANCH"
|
||||
echo "🐳 Container : $CONTAINER_NAME"
|
||||
echo "🔌 Port : $PORT"
|
||||
echo "📁 Folder : $(pwd)"
|
||||
[ "$ROLLBACK" = true ] && echo "⏪ Mode : ROLLBACK ke $PREVIOUS_IMAGE"
|
||||
echo ""
|
||||
|
||||
# ─── Pastikan config file ada ─────────────────────────────────────────────────
|
||||
CONFIG_FILE="infra/$ENV_MODE.yaml"
|
||||
if [ ! -f "$CONFIG_FILE" ]; then
|
||||
echo "❌ Config file '$CONFIG_FILE' tidak ditemukan."
|
||||
exit 1
|
||||
# ─── Konfirmasi production ────────────────────────────────────────────────────
|
||||
if [ "$ENV_MODE" = "production" ] && [ "$ASSUME_YES" != true ]; then
|
||||
read -r -p "⚠️ Ini PRODUCTION. Ketik 'production' untuk lanjut: " CONFIRM
|
||||
[ "$CONFIRM" = "production" ] || fail "Dibatalkan."
|
||||
fi
|
||||
|
||||
echo "🔄 Pulling latest code..."
|
||||
git pull
|
||||
run_container() {
|
||||
local image="$1"
|
||||
|
||||
echo "🛑 Stopping and removing old container..."
|
||||
docker rm -f "$CONTAINER_NAME" 2>/dev/null || true
|
||||
# Container production lama masih bernama 'apskel-pos'; staging tidak boleh menyentuhnya
|
||||
if [ "$ENV_MODE" = "production" ]; then
|
||||
docker rm -f "$LEGACY_CONTAINER" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
echo "🚀 Running container from $image..."
|
||||
docker run -d --name "$CONTAINER_NAME" \
|
||||
--restart unless-stopped \
|
||||
-p "$PORT:4000" \
|
||||
-e TZ=Asia/Jakarta \
|
||||
-e ENV_MODE="$ENV_MODE" \
|
||||
-v "$(pwd)/infra":/infra:ro \
|
||||
-v "$(pwd)/templates":/templates:ro \
|
||||
"$image" >/dev/null
|
||||
}
|
||||
|
||||
wait_healthy() {
|
||||
echo "🩺 Waiting for healthcheck..."
|
||||
for _ in $(seq 1 36); do
|
||||
local status
|
||||
status=$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "$CONTAINER_NAME" 2>/dev/null || echo "missing")
|
||||
case "$status" in
|
||||
healthy) return 0 ;;
|
||||
unhealthy|exited|dead|missing) return 1 ;;
|
||||
esac
|
||||
sleep 5
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# ─── Rollback ─────────────────────────────────────────────────────────────────
|
||||
if [ "$ROLLBACK" = true ]; then
|
||||
docker image inspect "$PREVIOUS_IMAGE" >/dev/null 2>&1 || fail "Image '$PREVIOUS_IMAGE' tidak ada, tidak bisa rollback."
|
||||
run_container "$PREVIOUS_IMAGE"
|
||||
wait_healthy || fail "Container hasil rollback tidak healthy. Cek: docker logs $CONTAINER_NAME"
|
||||
docker tag "$PREVIOUS_IMAGE" "$IMAGE_NAME"
|
||||
echo "✅ Rollback $ENV_MODE complete."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ─── Deploy ───────────────────────────────────────────────────────────────────
|
||||
echo "🔄 Pulling latest code (origin/$EXPECTED_BRANCH)..."
|
||||
git fetch origin "$EXPECTED_BRANCH"
|
||||
git merge --ff-only "origin/$EXPECTED_BRANCH"
|
||||
echo " Commit: $(git log -1 --format='%h %s')"
|
||||
|
||||
# Simpan image yang sedang jalan untuk rollback
|
||||
if docker image inspect "$IMAGE_NAME" >/dev/null 2>&1; then
|
||||
docker tag "$IMAGE_NAME" "$PREVIOUS_IMAGE"
|
||||
fi
|
||||
|
||||
echo "🐳 Building Docker image ($ENV_MODE)..."
|
||||
docker build --target production -t "$IMAGE_NAME" .
|
||||
|
||||
echo "🛑 Stopping and removing old container..."
|
||||
docker rm -f "$CONTAINER_NAME" 2>/dev/null || true
|
||||
run_container "$IMAGE_NAME"
|
||||
|
||||
echo "🚀 Running new container..."
|
||||
docker run -d --name "$CONTAINER_NAME" \
|
||||
-p "$PORT:4000" \
|
||||
-e TZ=Asia/Jakarta \
|
||||
-e ENV_MODE="$ENV_MODE" \
|
||||
-v "$(pwd)/infra":/infra:ro \
|
||||
-v "$(pwd)/templates":/templates:ro \
|
||||
"$IMAGE_NAME"
|
||||
if ! wait_healthy; then
|
||||
echo "❌ Container baru tidak healthy. Log terakhir:"
|
||||
docker logs --tail 50 "$CONTAINER_NAME" || true
|
||||
if docker image inspect "$PREVIOUS_IMAGE" >/dev/null 2>&1; then
|
||||
echo "⏪ Rolling back ke $PREVIOUS_IMAGE..."
|
||||
run_container "$PREVIOUS_IMAGE"
|
||||
wait_healthy || fail "Rollback juga tidak healthy. Cek manual: docker logs $CONTAINER_NAME"
|
||||
# Jangan biarkan image rusak jadi 'previous' di deploy berikutnya
|
||||
docker tag "$PREVIOUS_IMAGE" "$IMAGE_NAME"
|
||||
fail "Deploy gagal, $ENV_MODE sudah dikembalikan ke image sebelumnya."
|
||||
fi
|
||||
fail "Deploy gagal dan tidak ada image sebelumnya untuk rollback."
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "✅ Deployment $ENV_MODE complete."
|
||||
echo " Container : $CONTAINER_NAME"
|
||||
echo " Port : $PORT"
|
||||
echo " Rollback : $0 $ENV_MODE --rollback"
|
||||
|
||||
@@ -426,14 +426,11 @@ Semua nilai `data` berupa string. Push hanya sampai ke device yang terdaftar lew
|
||||
|
||||
### 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 |
|
||||
| --- | --- |
|
||||
| `GET /customer/points` | `GET /customer/wallet` → `point_balance` |
|
||||
| `GET /customer/tokens` | `GET /customer/wallet` → `coin_balance` |
|
||||
| `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` |
|
||||
| `total_points`, `points_history`, `last_updated` di `/customer/wallet` | `point_balance`, `recent_transactions` |
|
||||
|
||||
Panduan alur lengkap per tim ada di [`integration-enakpoint.md`](./integration-enakpoint.md).
|
||||
|
||||
@@ -474,16 +474,14 @@ Response:
|
||||
|
||||
```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", … },
|
||||
"coins_remaining": 7,
|
||||
"tokens_remaining": 7
|
||||
"coins_remaining": 7
|
||||
}
|
||||
```
|
||||
|
||||
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
|
||||
`tokens_remaining` hanya salinan untuk versi aplikasi lama.
|
||||
EnakCoin yang terpotong.
|
||||
|
||||
Di dashboard, `metadata.coin_cost` diisi per game dengan bilangan bulat ≥ 1.
|
||||
|
||||
@@ -497,13 +495,14 @@ pindah. Aplikasi baru jangan memakainya.
|
||||
| Lama | Ganti dengan |
|
||||
|---|---|
|
||||
| `GET /customer/points` | `GET /customer/wallet` (`point_balance`) |
|
||||
| `GET /customer/tokens` | `GET /customer/wallet` (`coin_balance`) |
|
||||
| `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` |
|
||||
| `total_points`, `points_history`, `last_updated` di `/customer/wallet` | `point_balance`, `recent_transactions` |
|
||||
|
||||
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"
|
||||
// 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