diff --git a/api/server/interface/api/srequest/payment_intent.go b/api/server/interface/api/srequest/payment_intent.go index 455b16d..7beec01 100644 --- a/api/server/interface/api/srequest/payment_intent.go +++ b/api/server/interface/api/srequest/payment_intent.go @@ -33,7 +33,8 @@ func (p *PaymentIntent) Validate() error { if p.Amount == nil { return merrors.InvalidArgument("amount is required", "intent.amount") } - if err := ValidateMoney(p.Amount); err != nil { + //TODO: collect supported currencies and validate against them + if err := ValidateMoney(p.Amount, nil); err != nil { return err } diff --git a/api/server/interface/api/srequest/payment_value_objects.go b/api/server/interface/api/srequest/payment_value_objects.go index 41732ee..f360f12 100644 --- a/api/server/interface/api/srequest/payment_value_objects.go +++ b/api/server/interface/api/srequest/payment_value_objects.go @@ -1,30 +1,76 @@ package srequest import ( + "regexp" + "strings" + "github.com/shopspring/decimal" "github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/model" ) -func ValidateMoney(m *model.Money) error { - if m.Amount == "" { +// 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}$`) + +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") } - if m.Currency == "" { + if strings.TrimSpace(m.Currency) == "" { return merrors.InvalidArgument("currency is required", "intent.currency") } - if _, err := decimal.NewFromString(m.Amount); err != nil { - return merrors.InvalidArgument("invalid amount decimal", "intent.amount") + // + // 2) Validate decimal amount + // + amount, err := decimal.NewFromString(m.Amount) + if err != nil { + return merrors.InvalidArgument("invalid decimal amount", "intent.amount") } - if len(m.Currency) != 3 { - return merrors.InvalidArgument("currency must be 3 letters", "intent.currency") + if amount.IsNegative() { + return merrors.InvalidArgument("amount must be >= 0", "intent.amount") } - for _, c := range m.Currency { - if c < 'A' || c > 'Z' { - return merrors.InvalidArgument("currency must be uppercase A-Z", "intent.currency") - } + + // + // 3) Normalize currency + // + cur := strings.ToUpper(strings.TrimSpace(m.Currency)) + + // + // 4) Syntax validation first — reject malformed tickers early + // + if !currencySyntax.MatchString(cur) { + return merrors.InvalidArgument( + "invalid currency format (must be A–Z0–9, length 2–10)", + "intent.currency", + ) + } + + // + // 5) Dictionary / environment 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 diff --git a/frontend/pshared/lib/models/resources.dart b/frontend/pshared/lib/models/resources.dart index 41cdedf..aaa1df7 100644 --- a/frontend/pshared/lib/models/resources.dart +++ b/frontend/pshared/lib/models/resources.dart @@ -82,6 +82,9 @@ enum ResourceType { @JsonValue('payment_methods') paymentMethods, + @JsonValue('payment_orchestrator') + paymentOrchestrator, + /// Represents permissions service @JsonValue('permissions') permissions,