payment quotation v2 + payment orchestration v2 draft

This commit is contained in:
Stephan D
2026-02-24 13:01:35 +01:00
parent 0646f55189
commit 6444813f38
289 changed files with 17005 additions and 16065 deletions

View 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,
}
}

View 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")
}
}

View File

@@ -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)

View File

@@ -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
)

View File

@@ -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=

View File

@@ -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

View File

@@ -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())