Revert "feat(loyalty): EnakPoint & EnakCoin" (#32)

This reverts merge commit 645da30, returning main to f0ff59f.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
efrilm
2026-09-30 15:16:15 +07:00
co-authored by Claude Opus 5.5
parent 645da3048e
commit 4e24f9bbb0
195 changed files with 2179 additions and 20060 deletions
+8 -8
View File
@@ -43,7 +43,7 @@ func (v *CampaignValidatorImpl) ValidateCreateCampaignRequest(req *contract.Crea
// Validate campaign type
if !v.isValidCampaignType(req.Type) {
return errors.New("invalid campaign type. Valid types are: REWARD, POINTS, COINS, MIXED"), constants.ValidationErrorCode
return errors.New("invalid campaign type. Valid types are: REWARD, POINTS, TOKENS, MIXED"), constants.ValidationErrorCode
}
// Validate date range
@@ -79,7 +79,7 @@ func (v *CampaignValidatorImpl) ValidateUpdateCampaignRequest(req *contract.Upda
// Validate campaign type
if !v.isValidCampaignType(req.Type) {
return errors.New("invalid campaign type. Valid types are: REWARD, POINTS, COINS, MIXED"), constants.ValidationErrorCode
return errors.New("invalid campaign type. Valid types are: REWARD, POINTS, TOKENS, MIXED"), constants.ValidationErrorCode
}
// Validate date range
@@ -113,7 +113,7 @@ func (v *CampaignValidatorImpl) ValidateListCampaignsRequest(req *contract.ListC
// Validate campaign type filter if provided
if req.Type != "" && !v.isValidCampaignType(req.Type) {
return errors.New("invalid campaign type filter. Valid types are: REWARD, POINTS, COINS, MIXED"), constants.ValidationErrorCode
return errors.New("invalid campaign type filter. Valid types are: REWARD, POINTS, TOKENS, MIXED"), constants.ValidationErrorCode
}
return nil, ""
@@ -223,7 +223,7 @@ func (v *CampaignValidatorImpl) ValidateListCampaignRulesRequest(req *contract.L
// Validate reward type filter if provided
if req.RewardType != "" && !v.isValidRewardType(req.RewardType) {
return errors.New("invalid reward type filter. Valid types are: POINTS, COINS, REWARD"), constants.ValidationErrorCode
return errors.New("invalid reward type filter. Valid types are: POINTS, TOKENS, REWARD"), constants.ValidationErrorCode
}
return nil, ""
@@ -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", "TOKENS", "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", "TOKENS", "REWARD"}
return contains(validTypes, rewardType)
}
@@ -293,11 +293,11 @@ func (v *CampaignValidatorImpl) validateCampaignRule(rule *contract.CampaignRule
// Validate reward type
if !v.isValidRewardType(rule.RewardType) {
return errors.New("invalid reward type in rule " + string(rune(ruleNumber)) + ". Valid types are: POINTS, COINS, REWARD")
return errors.New("invalid reward type in rule " + string(rune(ruleNumber)) + ". Valid types are: POINTS, TOKENS, REWARD")
}
// Validate reward value based on reward type
if rule.RewardType == "POINTS" || rule.RewardType == "COINS" || rule.RewardType == "TOKENS" {
if rule.RewardType == "POINTS" || rule.RewardType == "TOKENS" {
if rule.RewardValue == nil || *rule.RewardValue <= 0 {
return errors.New("reward value must be positive for " + rule.RewardType + " type in rule " + string(rune(ruleNumber)))
}
+166 -2
View File
@@ -9,6 +9,20 @@ import (
)
type GamificationValidator interface {
// Customer Points
ValidateCreateCustomerPointsRequest(req *contract.CreateCustomerPointsRequest) (error, string)
ValidateUpdateCustomerPointsRequest(req *contract.UpdateCustomerPointsRequest) (error, string)
ValidateListCustomerPointsRequest(req *contract.ListCustomerPointsRequest) (error, string)
ValidateAddCustomerPointsRequest(req *contract.AddCustomerPointsRequest) (error, string)
ValidateDeductCustomerPointsRequest(req *contract.DeductCustomerPointsRequest) (error, string)
// Customer Tokens
ValidateCreateCustomerTokensRequest(req *contract.CreateCustomerTokensRequest) (error, string)
ValidateUpdateCustomerTokensRequest(req *contract.UpdateCustomerTokensRequest) (error, string)
ValidateListCustomerTokensRequest(req *contract.ListCustomerTokensRequest) (error, string)
ValidateAddCustomerTokensRequest(req *contract.AddCustomerTokensRequest) (error, string)
ValidateDeductCustomerTokensRequest(req *contract.DeductCustomerTokensRequest) (error, string)
// Tiers
ValidateCreateTierRequest(req *contract.CreateTierRequest) (error, string)
ValidateUpdateTierRequest(req *contract.UpdateTierRequest) (error, string)
@@ -46,6 +60,152 @@ func NewGamificationValidator() *GamificationValidatorImpl {
}
}
// Customer Points Validators
func (v *GamificationValidatorImpl) ValidateCreateCustomerPointsRequest(req *contract.CreateCustomerPointsRequest) (error, string) {
if err := v.validate.Struct(req); err != nil {
return err, "VALIDATION_ERROR"
}
if req.Balance < 0 {
return errors.New("balance cannot be negative"), "INVALID_BALANCE"
}
return nil, ""
}
func (v *GamificationValidatorImpl) ValidateUpdateCustomerPointsRequest(req *contract.UpdateCustomerPointsRequest) (error, string) {
if err := v.validate.Struct(req); err != nil {
return err, "VALIDATION_ERROR"
}
if req.Balance < 0 {
return errors.New("balance cannot be negative"), "INVALID_BALANCE"
}
return nil, ""
}
func (v *GamificationValidatorImpl) ValidateListCustomerPointsRequest(req *contract.ListCustomerPointsRequest) (error, string) {
if err := v.validate.Struct(req); err != nil {
return err, "VALIDATION_ERROR"
}
if req.Page <= 0 {
req.Page = 1
}
if req.Limit <= 0 {
req.Limit = 10
}
if req.Limit > 100 {
req.Limit = 100
}
return nil, ""
}
func (v *GamificationValidatorImpl) ValidateAddCustomerPointsRequest(req *contract.AddCustomerPointsRequest) (error, string) {
if err := v.validate.Struct(req); err != nil {
return err, "VALIDATION_ERROR"
}
if req.Points <= 0 {
return errors.New("points must be greater than 0"), "INVALID_POINTS"
}
return nil, ""
}
func (v *GamificationValidatorImpl) ValidateDeductCustomerPointsRequest(req *contract.DeductCustomerPointsRequest) (error, string) {
if err := v.validate.Struct(req); err != nil {
return err, "VALIDATION_ERROR"
}
if req.Points <= 0 {
return errors.New("points must be greater than 0"), "INVALID_POINTS"
}
return nil, ""
}
// Customer Tokens Validators
func (v *GamificationValidatorImpl) ValidateCreateCustomerTokensRequest(req *contract.CreateCustomerTokensRequest) (error, string) {
if err := v.validate.Struct(req); err != nil {
return err, "VALIDATION_ERROR"
}
if req.Balance < 0 {
return errors.New("balance cannot be negative"), "INVALID_BALANCE"
}
validTokenTypes := []string{"SPIN", "RAFFLE", "MINIGAME"}
if !contains(validTokenTypes, req.TokenType) {
return errors.New("invalid token type"), "INVALID_TOKEN_TYPE"
}
return nil, ""
}
func (v *GamificationValidatorImpl) ValidateUpdateCustomerTokensRequest(req *contract.UpdateCustomerTokensRequest) (error, string) {
if err := v.validate.Struct(req); err != nil {
return err, "VALIDATION_ERROR"
}
if req.Balance < 0 {
return errors.New("balance cannot be negative"), "INVALID_BALANCE"
}
return nil, ""
}
func (v *GamificationValidatorImpl) ValidateListCustomerTokensRequest(req *contract.ListCustomerTokensRequest) (error, string) {
if err := v.validate.Struct(req); err != nil {
return err, "VALIDATION_ERROR"
}
if req.Page <= 0 {
req.Page = 1
}
if req.Limit <= 0 {
req.Limit = 10
}
if req.Limit > 100 {
req.Limit = 100
}
if req.TokenType != "" {
validTokenTypes := []string{"SPIN", "RAFFLE", "MINIGAME"}
if !contains(validTokenTypes, req.TokenType) {
return errors.New("invalid token type"), "INVALID_TOKEN_TYPE"
}
}
return nil, ""
}
func (v *GamificationValidatorImpl) ValidateAddCustomerTokensRequest(req *contract.AddCustomerTokensRequest) (error, string) {
if err := v.validate.Struct(req); err != nil {
return err, "VALIDATION_ERROR"
}
if req.Tokens <= 0 {
return errors.New("tokens must be greater than 0"), "INVALID_TOKENS"
}
return nil, ""
}
func (v *GamificationValidatorImpl) ValidateDeductCustomerTokensRequest(req *contract.DeductCustomerTokensRequest) (error, string) {
if err := v.validate.Struct(req); err != nil {
return err, "VALIDATION_ERROR"
}
if req.Tokens <= 0 {
return errors.New("tokens must be greater than 0"), "INVALID_TOKENS"
}
return nil, ""
}
// Tier Validators
func (v *GamificationValidatorImpl) ValidateCreateTierRequest(req *contract.CreateTierRequest) (error, string) {
if err := v.validate.Struct(req); err != nil {
@@ -276,8 +436,8 @@ func (v *GamificationValidatorImpl) ValidateCreateGamePlayRequest(req *contract.
return err, "VALIDATION_ERROR"
}
if req.CoinsUsed < 0 {
return errors.New("coins used cannot be negative"), "INVALID_COINS_USED"
if req.TokenUsed < 0 {
return errors.New("token used cannot be negative"), "INVALID_TOKEN_USED"
}
return nil, ""
@@ -306,6 +466,10 @@ func (v *GamificationValidatorImpl) ValidatePlayGameRequest(req *contract.PlayGa
return err, "VALIDATION_ERROR"
}
if req.TokenUsed < 0 {
return errors.New("token used cannot be negative"), "INVALID_TOKEN_USED"
}
return nil, ""
}
@@ -95,7 +95,6 @@ func (v *PaymentMethodValidatorImpl) isValidPaymentMethodType(paymentMethodType
string(constants.PaymentMethodTypeDigitalWallet),
string(constants.PaymentMethodTypeQR),
string(constants.PaymentMethodTypeEDC),
string(constants.PaymentMethodTypePoint),
}
for _, validType := range validTypes {
-66
View File
@@ -1,66 +0,0 @@
package validator
import (
"errors"
"strings"
"unicode/utf8"
"apskel-pos-be/internal/constants"
"apskel-pos-be/internal/contract"
)
// walletAdjustmentReasonLimit is the size of wallet_transactions.reason.
const walletAdjustmentReasonLimit = 255
// walletCashOutWords are reasons that describe turning balance into money. EnakPoint
// and EnakCoin can never be cashed out, and an adjustment must not be used to record
// that it happened (docs/prd-point-coin.md K7). Words like "tunai" are left out on
// purpose: "paid in cash but the points never arrived" is a legitimate reason.
var walletCashOutWords = []string{"pencairan", "cairkan", "cash out", "cashout", "withdraw"}
type WalletValidator interface {
ValidateAdjustRequest(req *contract.AdjustCustomerWalletRequest) (error, string)
}
type WalletValidatorImpl struct{}
func NewWalletValidator() *WalletValidatorImpl {
return &WalletValidatorImpl{}
}
func (v *WalletValidatorImpl) ValidateAdjustRequest(req *contract.AdjustCustomerWalletRequest) (error, string) {
if req == nil {
return errors.New("request body is required"), constants.MissingFieldErrorCode
}
req.Currency = strings.ToUpper(strings.TrimSpace(req.Currency))
if req.Currency == "" {
return errors.New("currency is required"), constants.MissingFieldErrorCode
}
if !constants.IsValidWalletCurrency(req.Currency) {
return errors.New("currency must be POINT or COIN"), constants.MalformedFieldErrorCode
}
if req.Amount == 0 {
return errors.New("amount must not be zero: use a positive amount to add and a negative one to take away"), constants.ValidationErrorCode
}
req.Reason = strings.TrimSpace(req.Reason)
if req.Reason == "" {
return errors.New("reason is required"), constants.MissingFieldErrorCode
}
if utf8.RuneCountInString(req.Reason) > walletAdjustmentReasonLimit {
return errors.New("reason must be at most 255 characters"), constants.MalformedFieldErrorCode
}
lower := strings.ToLower(req.Reason)
for _, word := range walletCashOutWords {
if strings.Contains(lower, word) {
return errors.New("EnakPoint and EnakCoin cannot be cashed out, so an adjustment cannot be for a cash-out"), constants.ValidationErrorCode
}
}
if len(req.IdempotencyKey) > 50 {
return errors.New("idempotency_key must be at most 50 characters"), constants.MalformedFieldErrorCode
}
return nil, ""
}
@@ -1,43 +0,0 @@
package validator
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
"apskel-pos-be/internal/constants"
"apskel-pos-be/internal/contract"
)
func TestWalletValidator_AdjustRequest(t *testing.T) {
v := NewWalletValidator()
ok := &contract.AdjustCustomerWalletRequest{Currency: " point ", Amount: -25, Reason: " komplain #45 "}
err, _ := v.ValidateAdjustRequest(ok)
assert.NoError(t, err)
assert.Equal(t, constants.WalletCurrencyPoint, ok.Currency, "currency is normalised")
assert.Equal(t, "komplain #45", ok.Reason, "reason is trimmed")
legit := &contract.AdjustCustomerWalletRequest{Currency: "COIN", Amount: 5, Reason: "Bayar tunai tapi poin tidak masuk"}
err, _ = v.ValidateAdjustRequest(legit)
assert.NoError(t, err, "mentioning a cash payment is not a cash-out")
for name, req := range map[string]*contract.AdjustCustomerWalletRequest{
"nil": nil,
"no currency": {Amount: 5, Reason: "x"},
"unknown currency": {Currency: "GOLD", Amount: 5, Reason: "x"},
"zero amount": {Currency: "POINT", Amount: 0, Reason: "x"},
"no reason": {Currency: "POINT", Amount: 5, Reason: " "},
"reason too long": {Currency: "POINT", Amount: 5, Reason: strings.Repeat("a", 256)},
"cash-out reason": {Currency: "POINT", Amount: -500, Reason: "Pencairan saldo ke rekening"},
"cash-out, English": {Currency: "POINT", Amount: -500, Reason: "customer asked to cash out"},
"long key": {Currency: "POINT", Amount: 5, Reason: "x", IdempotencyKey: strings.Repeat("k", 51)},
} {
t.Run(name, func(t *testing.T) {
err, code := v.ValidateAdjustRequest(req)
assert.Error(t, err)
assert.NotEmpty(t, code)
})
}
}