Staging #37
+1
-1
@@ -15,6 +15,6 @@ COPY --from=build /out/app /app
|
|||||||
ENV TZ=Asia/Jakarta
|
ENV TZ=Asia/Jakarta
|
||||||
EXPOSE 4000
|
EXPOSE 4000
|
||||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
|
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
|
USER appuser
|
||||||
ENTRYPOINT ["/app"]
|
ENTRYPOINT ["/app"]
|
||||||
|
|||||||
+137
-22
@@ -1,62 +1,177 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -euo pipefail
|
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"
|
APP_NAME="apskel-pos"
|
||||||
|
LEGACY_CONTAINER="apskel-pos" # nama lama sebelum nama container memakai environment
|
||||||
|
|
||||||
# ─── Deteksi environment dari branch aktif ───────────────────────────────────
|
usage() {
|
||||||
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
echo "Usage: $0 <staging|production> [--rollback] [--yes]"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
case "$CURRENT_BRANCH" in
|
fail() {
|
||||||
main)
|
echo "❌ $*"
|
||||||
ENV_MODE="production"
|
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"
|
PORT="4000"
|
||||||
;;
|
;;
|
||||||
staging)
|
staging)
|
||||||
ENV_MODE="staging"
|
EXPECTED_BRANCH="staging"
|
||||||
PORT="4001"
|
PORT="4001"
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo "❌ Branch '$CURRENT_BRANCH' tidak dikenali untuk deployment."
|
usage
|
||||||
echo " Gunakan branch 'main' (production) atau 'staging' (staging)."
|
|
||||||
exit 1
|
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
CONTAINER_NAME="$APP_NAME"
|
CONTAINER_NAME="$APP_NAME-$ENV_MODE"
|
||||||
IMAGE_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 "📦 Environment : $ENV_MODE"
|
||||||
echo "🌿 Branch : $CURRENT_BRANCH"
|
echo "🌿 Branch : $CURRENT_BRANCH"
|
||||||
echo "🐳 Container : $CONTAINER_NAME"
|
echo "🐳 Container : $CONTAINER_NAME"
|
||||||
echo "🔌 Port : $PORT"
|
echo "🔌 Port : $PORT"
|
||||||
|
echo "📁 Folder : $(pwd)"
|
||||||
|
[ "$ROLLBACK" = true ] && echo "⏪ Mode : ROLLBACK ke $PREVIOUS_IMAGE"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# ─── Pastikan config file ada ─────────────────────────────────────────────────
|
# ─── Konfirmasi production ────────────────────────────────────────────────────
|
||||||
CONFIG_FILE="infra/$ENV_MODE.yaml"
|
if [ "$ENV_MODE" = "production" ] && [ "$ASSUME_YES" != true ]; then
|
||||||
if [ ! -f "$CONFIG_FILE" ]; then
|
read -r -p "⚠️ Ini PRODUCTION. Ketik 'production' untuk lanjut: " CONFIRM
|
||||||
echo "❌ Config file '$CONFIG_FILE' tidak ditemukan."
|
[ "$CONFIRM" = "production" ] || fail "Dibatalkan."
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "🔄 Pulling latest code..."
|
run_container() {
|
||||||
git pull
|
local image="$1"
|
||||||
|
|
||||||
echo "🐳 Building Docker image ($ENV_MODE)..."
|
|
||||||
docker build --target production -t "$IMAGE_NAME" .
|
|
||||||
|
|
||||||
echo "🛑 Stopping and removing old container..."
|
echo "🛑 Stopping and removing old container..."
|
||||||
docker rm -f "$CONTAINER_NAME" 2>/dev/null || true
|
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 new container..."
|
echo "🚀 Running container from $image..."
|
||||||
docker run -d --name "$CONTAINER_NAME" \
|
docker run -d --name "$CONTAINER_NAME" \
|
||||||
|
--restart unless-stopped \
|
||||||
-p "$PORT:4000" \
|
-p "$PORT:4000" \
|
||||||
-e TZ=Asia/Jakarta \
|
-e TZ=Asia/Jakarta \
|
||||||
-e ENV_MODE="$ENV_MODE" \
|
-e ENV_MODE="$ENV_MODE" \
|
||||||
-v "$(pwd)/infra":/infra:ro \
|
-v "$(pwd)/infra":/infra:ro \
|
||||||
-v "$(pwd)/templates":/templates:ro \
|
-v "$(pwd)/templates":/templates:ro \
|
||||||
"$IMAGE_NAME"
|
"$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" .
|
||||||
|
|
||||||
|
run_container "$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 ""
|
||||||
echo "✅ Deployment $ENV_MODE complete."
|
echo "✅ Deployment $ENV_MODE complete."
|
||||||
echo " Container : $CONTAINER_NAME"
|
echo " Container : $CONTAINER_NAME"
|
||||||
echo " Port : $PORT"
|
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
|
### 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"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,8 +19,6 @@ type GamePlayResponse struct {
|
|||||||
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,
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ func SpinGameResponseDataModelToContract(data *models.SpinGameResponseData) *con
|
|||||||
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,
|
||||||
@@ -309,7 +308,6 @@ func PlayGameModelToResponse(model *models.PlayGameResponse) *contract.PlayGameR
|
|||||||
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