payment quotation v2 + payment orchestration v2 draft
This commit is contained in:
142
api/pkg/discovery/rail_vocab.go
Normal file
142
api/pkg/discovery/rail_vocab.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package discovery
|
||||
|
||||
import "strings"
|
||||
|
||||
const (
|
||||
RailCrypto = "CRYPTO"
|
||||
RailProviderSettlement = "PROVIDER_SETTLEMENT"
|
||||
RailLedger = "LEDGER"
|
||||
RailCardPayout = "CARD_PAYOUT"
|
||||
RailFiatOnRamp = "FIAT_ONRAMP"
|
||||
)
|
||||
|
||||
const (
|
||||
RailOperationDebit = "DEBIT"
|
||||
RailOperationCredit = "CREDIT"
|
||||
RailOperationExternalDebit = "EXTERNAL_DEBIT"
|
||||
RailOperationExternalCredit = "EXTERNAL_CREDIT"
|
||||
RailOperationMove = "MOVE"
|
||||
RailOperationSend = "SEND"
|
||||
RailOperationFee = "FEE"
|
||||
RailOperationObserveConfirm = "OBSERVE_CONFIRM"
|
||||
RailOperationFXConvert = "FX_CONVERT"
|
||||
RailOperationBlock = "BLOCK"
|
||||
RailOperationRelease = "RELEASE"
|
||||
)
|
||||
|
||||
var knownRails = map[string]struct{}{
|
||||
RailCrypto: {},
|
||||
RailProviderSettlement: {},
|
||||
RailLedger: {},
|
||||
RailCardPayout: {},
|
||||
RailFiatOnRamp: {},
|
||||
}
|
||||
|
||||
var knownRailOperations = map[string]struct{}{
|
||||
RailOperationDebit: {},
|
||||
RailOperationCredit: {},
|
||||
RailOperationExternalDebit: {},
|
||||
RailOperationExternalCredit: {},
|
||||
RailOperationMove: {},
|
||||
RailOperationSend: {},
|
||||
RailOperationFee: {},
|
||||
RailOperationObserveConfirm: {},
|
||||
RailOperationFXConvert: {},
|
||||
RailOperationBlock: {},
|
||||
RailOperationRelease: {},
|
||||
}
|
||||
|
||||
// NormalizeRail canonicalizes a rail token.
|
||||
func NormalizeRail(value string) string {
|
||||
return strings.ToUpper(strings.TrimSpace(value))
|
||||
}
|
||||
|
||||
// IsKnownRail reports whether the value is a recognized payment rail.
|
||||
func IsKnownRail(value string) bool {
|
||||
_, ok := knownRails[NormalizeRail(value)]
|
||||
return ok
|
||||
}
|
||||
|
||||
// NormalizeRailOperation canonicalizes a rail operation token.
|
||||
func NormalizeRailOperation(value string) string {
|
||||
clean := strings.ToUpper(strings.TrimSpace(value))
|
||||
if strings.HasPrefix(clean, "RAIL_OPERATION_") {
|
||||
clean = strings.TrimPrefix(clean, "RAIL_OPERATION_")
|
||||
}
|
||||
return clean
|
||||
}
|
||||
|
||||
// IsKnownRailOperation reports whether the value is a recognized rail operation.
|
||||
func IsKnownRailOperation(value string) bool {
|
||||
_, ok := knownRailOperations[NormalizeRailOperation(value)]
|
||||
return ok
|
||||
}
|
||||
|
||||
// ExpandRailOperation maps canonical and legacy names to normalized rail operations.
|
||||
func ExpandRailOperation(value string) []string {
|
||||
if op := NormalizeRailOperation(value); op != "" {
|
||||
if IsKnownRailOperation(op) {
|
||||
return []string{op}
|
||||
}
|
||||
}
|
||||
|
||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||
case "payin", "payin.crypto", "payin.fiat", "payin.card":
|
||||
return []string{RailOperationExternalDebit}
|
||||
case "payout", "payout.crypto", "payout.fiat", "payout.card":
|
||||
return []string{RailOperationExternalCredit, RailOperationSend}
|
||||
case "fee.send", "fees.send":
|
||||
return []string{RailOperationFee}
|
||||
case "observe.confirm", "observe_confirm":
|
||||
return []string{RailOperationObserveConfirm}
|
||||
case "funds.block", "hold.balance", "block":
|
||||
return []string{RailOperationBlock}
|
||||
case "funds.release", "release", "unblock":
|
||||
return []string{RailOperationRelease}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// NormalizeRailOperations canonicalizes and deduplicates rail operation values.
|
||||
func NormalizeRailOperations(values []string) []string {
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := make([]string, 0, len(values))
|
||||
seen := map[string]bool{}
|
||||
for _, value := range values {
|
||||
for _, op := range ExpandRailOperation(value) {
|
||||
if op == "" || seen[op] {
|
||||
continue
|
||||
}
|
||||
seen[op] = true
|
||||
result = append(result, op)
|
||||
}
|
||||
}
|
||||
if len(result) == 0 {
|
||||
return nil
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// CryptoRailGatewayOperations returns canonical operations for crypto rail gateways.
|
||||
func CryptoRailGatewayOperations() []string {
|
||||
return []string{
|
||||
RailOperationSend,
|
||||
RailOperationExternalDebit,
|
||||
RailOperationExternalCredit,
|
||||
RailOperationFee,
|
||||
RailOperationObserveConfirm,
|
||||
}
|
||||
}
|
||||
|
||||
// CardPayoutRailGatewayOperations returns canonical operations for card payout gateways.
|
||||
func CardPayoutRailGatewayOperations() []string {
|
||||
return []string{
|
||||
RailOperationSend,
|
||||
RailOperationExternalCredit,
|
||||
RailOperationObserveConfirm,
|
||||
}
|
||||
}
|
||||
49
api/pkg/discovery/rail_vocab_test.go
Normal file
49
api/pkg/discovery/rail_vocab_test.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package discovery
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestNormalizeRailOperations(t *testing.T) {
|
||||
got := NormalizeRailOperations([]string{
|
||||
"send",
|
||||
"payout.crypto",
|
||||
"observe.confirm",
|
||||
"unknown",
|
||||
"EXTERNAL_CREDIT",
|
||||
})
|
||||
|
||||
want := []string{
|
||||
RailOperationSend,
|
||||
RailOperationExternalCredit,
|
||||
RailOperationObserveConfirm,
|
||||
}
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("unexpected operations count: got=%d want=%d", len(got), len(want))
|
||||
}
|
||||
for i := range want {
|
||||
if got[i] != want[i] {
|
||||
t.Fatalf("unexpected operation[%d]: got=%q want=%q", i, got[i], want[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpandRailOperationLegacyAliases(t *testing.T) {
|
||||
got := ExpandRailOperation("payout.fiat")
|
||||
if len(got) != 2 {
|
||||
t.Fatalf("unexpected operations count: got=%d want=2", len(got))
|
||||
}
|
||||
if got[0] != RailOperationExternalCredit {
|
||||
t.Fatalf("unexpected first operation: got=%q want=%q", got[0], RailOperationExternalCredit)
|
||||
}
|
||||
if got[1] != RailOperationSend {
|
||||
t.Fatalf("unexpected second operation: got=%q want=%q", got[1], RailOperationSend)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsKnownRail(t *testing.T) {
|
||||
if !IsKnownRail("crypto") {
|
||||
t.Fatalf("expected crypto rail to be known")
|
||||
}
|
||||
if IsKnownRail("telegram") {
|
||||
t.Fatalf("did not expect telegram rail to be known")
|
||||
}
|
||||
}
|
||||
@@ -229,7 +229,7 @@ func normalizeEntry(entry RegistryEntry) RegistryEntry {
|
||||
entry.InstanceID = entry.ID
|
||||
}
|
||||
entry.Service = strings.TrimSpace(entry.Service)
|
||||
entry.Rail = strings.ToUpper(strings.TrimSpace(entry.Rail))
|
||||
entry.Rail = NormalizeRail(entry.Rail)
|
||||
entry.Network = strings.ToUpper(strings.TrimSpace(entry.Network))
|
||||
entry.Operations = normalizeStrings(entry.Operations, false)
|
||||
entry.CurrencyMeta = normalizeCurrencyAnnouncements(entry.CurrencyMeta)
|
||||
@@ -259,7 +259,7 @@ func normalizeAnnouncement(announce Announcement) Announcement {
|
||||
announce.InstanceID = announce.ID
|
||||
}
|
||||
announce.Service = strings.TrimSpace(announce.Service)
|
||||
announce.Rail = strings.ToUpper(strings.TrimSpace(announce.Rail))
|
||||
announce.Rail = NormalizeRail(announce.Rail)
|
||||
announce.Operations = normalizeStrings(announce.Operations, false)
|
||||
announce.Currencies = normalizeCurrencyAnnouncements(announce.Currencies)
|
||||
announce.InvokeURI = strings.TrimSpace(announce.InvokeURI)
|
||||
|
||||
@@ -9,7 +9,7 @@ require (
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/mattn/go-colorable v0.1.14
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
github.com/nats-io/nats.go v1.48.0
|
||||
github.com/nats-io/nats.go v1.49.0
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/testcontainers/testcontainers-go v0.33.0
|
||||
@@ -92,6 +92,6 @@ require (
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260223185530-2f722ef697dc // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -106,8 +106,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/nats-io/nats.go v1.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U=
|
||||
github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
|
||||
github.com/nats-io/nats.go v1.49.0 h1:yh/WvY59gXqYpgl33ZI+XoVPKyut/IcEaqtsiuTJpoE=
|
||||
github.com/nats-io/nats.go v1.49.0/go.mod h1:fDCn3mN5cY8HooHwE2ukiLb4p4G4ImmzvXyJt+tGwdw=
|
||||
github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4=
|
||||
github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs=
|
||||
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||
@@ -271,8 +271,8 @@ gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260223185530-2f722ef697dc h1:51Wupg8spF+5FC6D+iMKbOddFjMckETnNnEiZ+HX37s=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260223185530-2f722ef697dc/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
|
||||
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
|
||||
@@ -20,9 +20,9 @@ import (
|
||||
type natsSubscriotions = map[string]*TopicSubscription
|
||||
|
||||
type NatsBroker struct {
|
||||
logger mlogger.Logger
|
||||
nc *nats.Conn
|
||||
js nats.JetStreamContext
|
||||
logger *zap.Logger
|
||||
topicSubs natsSubscriotions
|
||||
mu sync.Mutex
|
||||
bufferSize int
|
||||
@@ -73,7 +73,7 @@ func sanitizeNATSURL(rawURL string) string {
|
||||
// loadEnv gathers and validates connection details from environment variables
|
||||
// listed in the Settings struct. Invalid or missing values surface as a typed
|
||||
// InvalidArgument error so callers can decide how to handle them.
|
||||
func loadEnv(settings *nc.Settings, l *zap.Logger) (*envConfig, error) {
|
||||
func loadEnv(settings *nc.Settings, l mlogger.Logger) (*envConfig, error) {
|
||||
get := func(key, label string) (string, error) {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v, nil
|
||||
|
||||
@@ -3,7 +3,6 @@ package grpcapp
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -203,16 +202,16 @@ func (a *App[T]) Start() error {
|
||||
}
|
||||
|
||||
if addr := a.grpc.Addr(); addr != nil {
|
||||
a.logger.Info(fmt.Sprintf("%s gRPC server started", a.name), zap.String("network", addr.Network()), zap.String("address", addr.String()), zap.Bool("debug_mode", a.debug))
|
||||
a.logger.Info("Server started", zap.String("server_name", a.name), zap.String("network", addr.Network()), zap.String("address", addr.String()), zap.Bool("debug_mode", a.debug))
|
||||
} else {
|
||||
a.logger.Info(fmt.Sprintf("%s gRPC server started", a.name), zap.Bool("debug_mode", a.debug))
|
||||
a.logger.Info("Server started", zap.String("server_name", a.name), zap.Bool("debug_mode", a.debug))
|
||||
}
|
||||
|
||||
err = <-a.grpc.Done()
|
||||
if err != nil && !errors.Is(err, context.Canceled) {
|
||||
a.logger.Error("GRPC server stopped with error", zap.Error(err))
|
||||
a.logger.Error("Server stopped with error", zap.Error(err))
|
||||
} else {
|
||||
a.logger.Info("GRPC server finished")
|
||||
a.logger.Info("Server finished")
|
||||
}
|
||||
|
||||
a.cleanup(context.Background())
|
||||
|
||||
Reference in New Issue
Block a user