Merge pull request 'fixed currency pair validation' (#85) from currency-84 into main
Some checks failed
ci/woodpecker/push/chain_gateway Pipeline is pending
ci/woodpecker/push/db Pipeline is pending
ci/woodpecker/push/frontend Pipeline is pending
ci/woodpecker/push/fx_ingestor Pipeline is pending
ci/woodpecker/push/fx_oracle Pipeline is pending
ci/woodpecker/push/ledger Pipeline is pending
ci/woodpecker/push/mntx_gateway Pipeline is pending
ci/woodpecker/push/nats Pipeline is pending
ci/woodpecker/push/notification Pipeline is pending
ci/woodpecker/push/payments_orchestrator Pipeline is pending
ci/woodpecker/push/billing_fees Pipeline failed
ci/woodpecker/push/bff Pipeline failed

Reviewed-on: #85
This commit was merged in pull request #85.
This commit is contained in:
2025-12-11 22:27:42 +00:00

View File

@@ -20,41 +20,17 @@ type AssetResolver interface {
// Precompile regex for efficiency. // Precompile regex for efficiency.
var currencySyntax = regexp.MustCompile(`^[A-Z0-9]{2,10}$`) var currencySyntax = regexp.MustCompile(`^[A-Z0-9]{2,10}$`)
func ValidateMoney(m *model.Money, assetResolver AssetResolver) error { // ValidateCurrency validates currency syntax and checks dictionary via assetResolver.
if m == nil { func ValidateCurrency(cur string, assetResolver AssetResolver) error {
return merrors.InvalidArgument("money is required", "intent.amount") // Basic presence
} if strings.TrimSpace(cur) == "" {
//
// 1) Basic presence
//
if strings.TrimSpace(m.Amount) == "" {
return merrors.InvalidArgument("amount is required", "intent.amount")
}
if strings.TrimSpace(m.Currency) == "" {
return merrors.InvalidArgument("currency is required", "intent.currency") return merrors.InvalidArgument("currency is required", "intent.currency")
} }
// // Normalize
// 2) Validate decimal amount cur = strings.ToUpper(strings.TrimSpace(cur))
//
amount, err := decimal.NewFromString(m.Amount)
if err != nil {
return merrors.InvalidArgument("invalid decimal amount", "intent.amount")
}
if amount.IsNegative() { // Syntax check
return merrors.InvalidArgument("amount must be >= 0", "intent.amount")
}
//
// 3) Normalize currency
//
cur := strings.ToUpper(strings.TrimSpace(m.Currency))
//
// 4) Syntax validation first — reject malformed tickers early
//
if !currencySyntax.MatchString(cur) { if !currencySyntax.MatchString(cur) {
return merrors.InvalidArgument( return merrors.InvalidArgument(
"invalid currency format (must be AZ09, length 210)", "invalid currency format (must be AZ09, length 210)",
@@ -62,9 +38,7 @@ func ValidateMoney(m *model.Money, assetResolver AssetResolver) error {
) )
} }
// // Dictionary validation
// 5) Dictionary / environment validation
//
if assetResolver == nil { if assetResolver == nil {
return merrors.InvalidArgument("asset resolver is not configured", "intent.currency") return merrors.InvalidArgument("asset resolver is not configured", "intent.currency")
} }
@@ -76,37 +50,48 @@ func ValidateMoney(m *model.Money, assetResolver AssetResolver) error {
return nil 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 { type CurrencyPair struct {
Base string `json:"base"` Base string `json:"base"`
Quote string `json:"quote"` Quote string `json:"quote"`
} }
func (p *CurrencyPair) Validate() error { func (p *CurrencyPair) Validate() error {
if p.Base == "" { if p == nil {
return merrors.InvalidArgument("base currency is required", "intent.fx.pair.base") return merrors.InvalidArgument("currency pair is required", "currncy_pair")
} }
if p.Quote == "" { if err := ValidateCurrency(p.Base, nil); err != nil {
return merrors.InvalidArgument("quote currency is required", "intent.fx.pair.quote") return merrors.InvalidArgument("invalid base currency in pair: "+err.Error(), "currency_pair.base")
} }
if err := ValidateCurrency(p.Quote, nil); err != nil {
if len(p.Base) != 3 { return merrors.InvalidArgument("invalid quote currency in pair: "+err.Error(), "currency_pair.quote")
return merrors.InvalidArgument("base currency must be 3 letters", "intent.fx.pair.base")
} }
if len(p.Quote) != 3 {
return merrors.InvalidArgument("quote currency must be 3 letters", "intent.fx.pair.quote")
}
for _, c := range p.Base {
if c < 'A' || c > 'Z' {
return merrors.InvalidArgument("base currency must be uppercase A-Z", "intent.fx.pair.base")
}
}
for _, c := range p.Quote {
if c < 'A' || c > 'Z' {
return merrors.InvalidArgument("quote currency must be uppercase A-Z", "intent.fx.pair.quote")
}
}
return nil return nil
} }