Files
sendico/api/server/interface/api/srequest/payment_value_objects.go
2025-12-11 23:55:04 +01:00

132 lines
3.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package srequest
import (
"regexp"
"strings"
"github.com/shopspring/decimal"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/model"
)
// AssetResolver defines environment-specific supported assets.
// Implementations should check:
// - fiat assets (ISO-4217)
// - crypto assets supported by gateways / FX providers
type AssetResolver interface {
IsSupported(ticker string) bool
}
// Precompile regex for efficiency.
var currencySyntax = regexp.MustCompile(`^[A-Z0-9]{2,10}$`)
// ValidateCurrency validates currency syntax and checks dictionary via assetResolver.
func ValidateCurrency(cur string, assetResolver AssetResolver) error {
// Basic presence
if strings.TrimSpace(cur) == "" {
return merrors.InvalidArgument("currency is required", "intent.currency")
}
// Normalize
cur = strings.ToUpper(strings.TrimSpace(cur))
// Syntax check
if !currencySyntax.MatchString(cur) {
return merrors.InvalidArgument(
"invalid currency format (must be AZ09, length 210)",
"intent.currency",
)
}
// Dictionary validation
if assetResolver == nil {
return merrors.InvalidArgument("asset resolver is not configured", "intent.currency")
}
if !assetResolver.IsSupported(cur) {
return merrors.InvalidArgument("unsupported currency/asset", "intent.currency")
}
return nil
}
func ValidateMoney(m *model.Money, assetResolver AssetResolver) error {
if m == nil {
return merrors.InvalidArgument("money is required", "intent.amount")
}
// 1) Basic presence
if strings.TrimSpace(m.Amount) == "" {
return merrors.InvalidArgument("amount is required", "intent.amount")
}
// 2) Validate decimal amount
amount, err := decimal.NewFromString(m.Amount)
if err != nil {
return merrors.InvalidArgument("invalid decimal amount", "intent.amount")
}
if amount.IsNegative() {
return merrors.InvalidArgument("amount must be >= 0", "intent.amount")
}
// 3) Validate currency via helper
if err := ValidateCurrency(m.Currency, assetResolver); err != nil {
return err
}
return nil
}
type CurrencyPair struct {
Base string `json:"base"`
Quote string `json:"quote"`
}
func (p *CurrencyPair) Validate() error {
if p == nil {
return merrors.InvalidArgument("currency pair is required", "currncy_pair")
}
if err := ValidateCurrency(p.Base, &AssetResolverStub{}); err != nil {
return merrors.InvalidArgument("invalid base currency in pair: "+err.Error(), "currency_pair.base")
}
if err := ValidateCurrency(p.Quote, &AssetResolverStub{}); err != nil {
return merrors.InvalidArgument("invalid quote currency in pair: "+err.Error(), "currency_pair.quote")
}
return nil
}
type FXIntent struct {
Pair *CurrencyPair `json:"pair,omitempty"`
Side FXSide `json:"side,omitempty"`
Firm bool `json:"firm,omitempty"`
TTLms int64 `json:"ttl_ms,omitempty"`
PreferredProvider string `json:"preferred_provider,omitempty"`
MaxAgeMs int32 `json:"max_age_ms,omitempty"`
}
func (fx *FXIntent) Validate() error {
if fx.Pair != nil {
if err := fx.Pair.Validate(); err != nil {
return err
}
}
var zeroSide FXSide
if fx.Side == zeroSide {
return merrors.InvalidArgument("fx side is required", "intent.fx.side")
}
if fx.TTLms < 0 {
return merrors.InvalidArgument("fx ttl_ms cannot be negative", "intent.fx.ttl_ms")
}
if fx.TTLms == 0 && fx.Firm {
return merrors.InvalidArgument("firm quote requires positive ttl_ms", "intent.fx.ttl_ms")
}
if fx.MaxAgeMs < 0 {
return merrors.InvalidArgument("fx max_age_ms cannot be negative", "intent.fx.max_age_ms")
}
return nil
}