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, "" }