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 { 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 }