182 lines
4.3 KiB
Go
182 lines
4.3 KiB
Go
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
|
|
}
|