Files
sendico/api/gateway/tgsettle/internal/service/treasury/validator.go
2026-03-04 20:01:37 +01:00

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
}