Treasury bot + ledger fix
This commit is contained in:
181
api/gateway/tgsettle/internal/service/treasury/validator.go
Normal file
181
api/gateway/tgsettle/internal/service/treasury/validator.go
Normal file
@@ -0,0 +1,181 @@
|
||||
package treasury
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/big"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tech/sendico/gateway/tgsettle/storage"
|
||||
storagemodel "github.com/tech/sendico/gateway/tgsettle/storage/model"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
)
|
||||
|
||||
var treasuryAmountPattern = regexp.MustCompile(`^[0-9]+(\.[0-9]+)?$`)
|
||||
|
||||
type LimitKind string
|
||||
|
||||
const (
|
||||
LimitKindPerOperation LimitKind = "per_operation"
|
||||
LimitKindDaily LimitKind = "daily"
|
||||
)
|
||||
|
||||
type LimitError struct {
|
||||
Kind LimitKind
|
||||
Max string
|
||||
}
|
||||
|
||||
func (e *LimitError) Error() string {
|
||||
if e == nil {
|
||||
return "limit exceeded"
|
||||
}
|
||||
switch e.Kind {
|
||||
case LimitKindPerOperation:
|
||||
return "max amount per operation exceeded"
|
||||
case LimitKindDaily:
|
||||
return "max daily amount exceeded"
|
||||
default:
|
||||
return "limit exceeded"
|
||||
}
|
||||
}
|
||||
|
||||
func (e *LimitError) LimitKind() string {
|
||||
if e == nil {
|
||||
return ""
|
||||
}
|
||||
return string(e.Kind)
|
||||
}
|
||||
|
||||
func (e *LimitError) LimitMax() string {
|
||||
if e == nil {
|
||||
return ""
|
||||
}
|
||||
return e.Max
|
||||
}
|
||||
|
||||
type Validator struct {
|
||||
repo storage.TreasuryRequestsStore
|
||||
|
||||
maxPerOperation *big.Rat
|
||||
maxDaily *big.Rat
|
||||
|
||||
maxPerOperationRaw string
|
||||
maxDailyRaw string
|
||||
}
|
||||
|
||||
func NewValidator(repo storage.TreasuryRequestsStore, maxPerOperation string, maxDaily string) (*Validator, error) {
|
||||
validator := &Validator{
|
||||
repo: repo,
|
||||
maxPerOperationRaw: strings.TrimSpace(maxPerOperation),
|
||||
maxDailyRaw: strings.TrimSpace(maxDaily),
|
||||
}
|
||||
if validator.maxPerOperationRaw != "" {
|
||||
value, err := parseAmountRat(validator.maxPerOperationRaw)
|
||||
if err != nil {
|
||||
return nil, merrors.InvalidArgument("treasury max_amount_per_operation is invalid", "treasury.limits.max_amount_per_operation")
|
||||
}
|
||||
validator.maxPerOperation = value
|
||||
}
|
||||
if validator.maxDailyRaw != "" {
|
||||
value, err := parseAmountRat(validator.maxDailyRaw)
|
||||
if err != nil {
|
||||
return nil, merrors.InvalidArgument("treasury max_daily_amount is invalid", "treasury.limits.max_daily_amount")
|
||||
}
|
||||
validator.maxDaily = value
|
||||
}
|
||||
return validator, nil
|
||||
}
|
||||
|
||||
func (v *Validator) MaxPerOperation() string {
|
||||
if v == nil {
|
||||
return ""
|
||||
}
|
||||
return v.maxPerOperationRaw
|
||||
}
|
||||
|
||||
func (v *Validator) MaxDaily() string {
|
||||
if v == nil {
|
||||
return ""
|
||||
}
|
||||
return v.maxDailyRaw
|
||||
}
|
||||
|
||||
func (v *Validator) ValidateAmount(amount string) (*big.Rat, string, error) {
|
||||
amount = strings.TrimSpace(amount)
|
||||
value, err := parseAmountRat(amount)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if v != nil && v.maxPerOperation != nil && value.Cmp(v.maxPerOperation) > 0 {
|
||||
return nil, "", &LimitError{
|
||||
Kind: LimitKindPerOperation,
|
||||
Max: v.maxPerOperationRaw,
|
||||
}
|
||||
}
|
||||
return value, amount, nil
|
||||
}
|
||||
|
||||
func (v *Validator) ValidateDailyLimit(ctx context.Context, ledgerAccountID string, amount *big.Rat, now time.Time) error {
|
||||
if v == nil || v.maxDaily == nil || v.repo == nil {
|
||||
return nil
|
||||
}
|
||||
if amount == nil {
|
||||
return merrors.InvalidArgument("amount is required", "amount")
|
||||
}
|
||||
dayStart := time.Date(now.UTC().Year(), now.UTC().Month(), now.UTC().Day(), 0, 0, 0, 0, time.UTC)
|
||||
dayEnd := dayStart.Add(24 * time.Hour)
|
||||
|
||||
records, err := v.repo.ListByAccountAndStatuses(
|
||||
ctx,
|
||||
ledgerAccountID,
|
||||
[]storagemodel.TreasuryRequestStatus{
|
||||
storagemodel.TreasuryRequestStatusCreated,
|
||||
storagemodel.TreasuryRequestStatusConfirmed,
|
||||
storagemodel.TreasuryRequestStatusScheduled,
|
||||
storagemodel.TreasuryRequestStatusExecuted,
|
||||
},
|
||||
dayStart,
|
||||
dayEnd,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
total := new(big.Rat)
|
||||
for _, record := range records {
|
||||
if record == nil {
|
||||
continue
|
||||
}
|
||||
next, err := parseAmountRat(record.Amount)
|
||||
if err != nil {
|
||||
return merrors.Internal("treasury request amount is invalid")
|
||||
}
|
||||
total.Add(total, next)
|
||||
}
|
||||
total.Add(total, amount)
|
||||
if total.Cmp(v.maxDaily) > 0 {
|
||||
return &LimitError{
|
||||
Kind: LimitKindDaily,
|
||||
Max: v.maxDailyRaw,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseAmountRat(value string) (*big.Rat, error) {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return nil, merrors.InvalidArgument("amount is required", "amount")
|
||||
}
|
||||
if !treasuryAmountPattern.MatchString(value) {
|
||||
return nil, merrors.InvalidArgument("amount format is invalid", "amount")
|
||||
}
|
||||
amount := new(big.Rat)
|
||||
if _, ok := amount.SetString(value); !ok {
|
||||
return nil, merrors.InvalidArgument("amount format is invalid", "amount")
|
||||
}
|
||||
if amount.Sign() <= 0 {
|
||||
return nil, merrors.InvalidArgument("amount must be positive", "amount")
|
||||
}
|
||||
return amount, nil
|
||||
}
|
||||
Reference in New Issue
Block a user