fixed rail & operation names

This commit is contained in:
Stephan D
2026-02-27 02:33:40 +01:00
parent 82cf91e703
commit 747153bdbf
73 changed files with 877 additions and 667 deletions

View File

@@ -12,20 +12,32 @@ require (
)
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect
github.com/casbin/casbin/v2 v2.135.0 // indirect
github.com/casbin/govaluate v1.10.0 // indirect
github.com/casbin/mongodb-adapter/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/klauspost/compress v1.18.4 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nats-io/nats.go v1.49.0 // indirect
github.com/nats-io/nkeys v0.4.15 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/prometheus/client_golang v1.23.2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/procfs v0.20.0 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.2.0 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.34.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
)

View File

@@ -4,6 +4,8 @@ github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs=
github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
@@ -53,6 +55,10 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54 h1:mFWunSatvkQQDhpdyuFAYwyAan3hzCuma+Pz8sqvOfg=
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
@@ -77,6 +83,14 @@ github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
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.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=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
@@ -87,6 +101,16 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
github.com/prometheus/procfs v0.20.0 h1:AA7aCvjxwAquZAlonN7888f2u4IN8WVeFgBi4k82M4Q=
github.com/prometheus/procfs v0.20.0/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
@@ -136,6 +160,8 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
@@ -171,5 +197,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -2,6 +2,7 @@ package model
import (
"fmt"
"github.com/tech/sendico/pkg/discovery"
"strings"
"github.com/shopspring/decimal"
@@ -110,7 +111,7 @@ func gatewayAllowsAction(operations []RailOperation, cap RailCapabilities, actio
func capabilityAllowsAction(cap RailCapabilities, action RailOperation, dir GatewayDirection) bool {
switch action {
case RailOperationSend:
case discovery.RailOperationSend:
switch dir {
case GatewayDirectionOut:
return cap.CanPayOut
@@ -119,7 +120,7 @@ func capabilityAllowsAction(cap RailCapabilities, action RailOperation, dir Gate
default:
return cap.CanPayIn || cap.CanPayOut
}
case RailOperationExternalDebit, RailOperationExternalCredit:
case discovery.RailOperationExternalDebit, discovery.RailOperationExternalCredit:
switch dir {
case GatewayDirectionOut:
return cap.CanPayOut
@@ -128,13 +129,13 @@ func capabilityAllowsAction(cap RailCapabilities, action RailOperation, dir Gate
default:
return cap.CanPayIn || cap.CanPayOut
}
case RailOperationFee:
case discovery.RailOperationFee:
return cap.CanSendFee
case RailOperationObserveConfirm:
case discovery.RailOperationObserveConfirm:
return cap.RequiresObserveConfirm
case RailOperationBlock:
case discovery.RailOperationBlock:
return cap.CanBlock
case RailOperationRelease:
case discovery.RailOperationRelease:
return cap.CanRelease
default:
return true
@@ -143,7 +144,7 @@ func capabilityAllowsAction(cap RailCapabilities, action RailOperation, dir Gate
func operationsAllowAction(operations []RailOperation, action RailOperation, dir GatewayDirection) bool {
action = ParseRailOperation(string(action))
if action == RailOperationUnspecified {
if action == discovery.RailOperationUnspecified {
return false
}
@@ -152,20 +153,20 @@ func operationsAllowAction(operations []RailOperation, action RailOperation, dir
}
switch action {
case RailOperationSend:
case discovery.RailOperationSend:
switch dir {
case GatewayDirectionIn:
return HasRailOperation(operations, RailOperationExternalDebit)
return HasRailOperation(operations, discovery.RailOperationExternalDebit)
case GatewayDirectionOut:
return HasRailOperation(operations, RailOperationExternalCredit)
return HasRailOperation(operations, discovery.RailOperationExternalCredit)
default:
return HasRailOperation(operations, RailOperationExternalDebit) ||
HasRailOperation(operations, RailOperationExternalCredit)
return HasRailOperation(operations, discovery.RailOperationExternalDebit) ||
HasRailOperation(operations, discovery.RailOperationExternalCredit)
}
case RailOperationExternalDebit:
return HasRailOperation(operations, RailOperationSend)
case RailOperationExternalCredit:
return HasRailOperation(operations, RailOperationSend)
case discovery.RailOperationExternalDebit:
return HasRailOperation(operations, discovery.RailOperationSend)
case discovery.RailOperationExternalCredit:
return HasRailOperation(operations, discovery.RailOperationSend)
default:
return false
}
@@ -181,7 +182,7 @@ func amountWithinLimits(gw *GatewayInstanceDescriptor, limits Limits, currency s
if override, ok := limits.CurrencyLimits[currency]; ok {
min = firstLimitValue(override.MinAmount, min)
max = firstLimitValue(override.MaxAmount, max)
if action == RailOperationFee {
if action == discovery.RailOperationFee {
maxFee = firstLimitValue(override.MaxFee, maxFee)
}
}
@@ -206,7 +207,7 @@ func amountWithinLimits(gw *GatewayInstanceDescriptor, limits Limits, currency s
return gatewayIneligible(gw, fmt.Sprintf("amount %s %s exceeds per-tx max limit %s", amount.String(), currency, val.String()))
}
}
if action == RailOperationFee && maxFee != "" {
if action == discovery.RailOperationFee && maxFee != "" {
if val, err := decimal.NewFromString(maxFee); err == nil && amount.GreaterThan(val) {
return gatewayIneligible(gw, fmt.Sprintf("fee amount %s %s exceeds max fee limit %s", amount.String(), currency, val.String()))
}

View File

@@ -1,6 +1,7 @@
package model
import (
"github.com/tech/sendico/pkg/discovery"
"testing"
"github.com/shopspring/decimal"
@@ -10,14 +11,14 @@ func TestIsGatewayEligible_AllowsMatchingGateway(t *testing.T) {
gw := &GatewayInstanceDescriptor{
ID: "gw-1",
InstanceID: "inst-1",
Rail: RailCrypto,
Rail: discovery.RailCrypto,
Network: "TRON",
Currencies: []string{"USDT"},
Operations: []RailOperation{RailOperationSend, RailOperationExternalCredit},
Operations: []RailOperation{discovery.RailOperationSend, discovery.RailOperationExternalCredit},
IsEnabled: true,
}
err := IsGatewayEligible(gw, RailCrypto, "TRON", "USDT", RailOperationSend, GatewayDirectionOut, decimal.RequireFromString("10"))
err := IsGatewayEligible(gw, discovery.RailCrypto, "TRON", "USDT", discovery.RailOperationSend, GatewayDirectionOut, decimal.RequireFromString("10"))
if err != nil {
t.Fatalf("expected gateway to be eligible, got err=%v", err)
}
@@ -27,21 +28,21 @@ func TestIsGatewayEligible_RejectsNetworkMismatch(t *testing.T) {
gw := &GatewayInstanceDescriptor{
ID: "gw-1",
InstanceID: "inst-1",
Rail: RailCrypto,
Rail: discovery.RailCrypto,
Network: "ETH",
Currencies: []string{"USDT"},
Operations: []RailOperation{RailOperationSend},
Operations: []RailOperation{discovery.RailOperationSend},
IsEnabled: true,
}
err := IsGatewayEligible(gw, RailCrypto, "TRON", "USDT", RailOperationSend, GatewayDirectionOut, decimal.RequireFromString("10"))
err := IsGatewayEligible(gw, discovery.RailCrypto, "TRON", "USDT", discovery.RailOperationSend, GatewayDirectionOut, decimal.RequireFromString("10"))
if err == nil {
t.Fatalf("expected network mismatch error")
}
}
func TestNoEligibleGatewayMessage(t *testing.T) {
got := NoEligibleGatewayMessage("tron", "usdt", RailOperationSend, GatewayDirectionOut)
got := NoEligibleGatewayMessage("tron", "usdt", discovery.RailOperationSend, GatewayDirectionOut)
want := "plan builder: no eligible gateway found for TRON USDT SEND for direction out"
if got != want {
t.Fatalf("unexpected message: got=%q want=%q", got, want)

View File

@@ -5,6 +5,7 @@ import (
"time"
"github.com/tech/sendico/pkg/db/storable"
"github.com/tech/sendico/pkg/discovery"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/model/account_role"
"github.com/tech/sendico/pkg/mservice"
@@ -79,35 +80,10 @@ const (
)
// Rail identifies a payment rail for orchestration.
type Rail string
const (
RailUnspecified Rail = "UNSPECIFIED"
RailCrypto Rail = "CRYPTO"
RailProviderSettlement Rail = "SETTLEMENT"
RailLedger Rail = "LEDGER"
RailCardPayout Rail = "CARD"
RailFiatOnRamp Rail = "ONRAMP"
RailFiatOffRamp Rail = "OFFRAMP"
)
type Rail = discovery.Rail
// RailOperation identifies an explicit action within a payment plan.
type RailOperation string
const (
RailOperationUnspecified RailOperation = "UNSPECIFIED"
RailOperationDebit RailOperation = "DEBIT"
RailOperationCredit RailOperation = "CREDIT"
RailOperationExternalDebit RailOperation = "EXTERNAL_DEBIT"
RailOperationExternalCredit RailOperation = "EXTERNAL_CREDIT"
RailOperationMove RailOperation = "MOVE"
RailOperationSend RailOperation = "SEND"
RailOperationFee RailOperation = "FEE"
RailOperationObserveConfirm RailOperation = "OBSERVE_CONFIRM"
RailOperationFXConvert RailOperation = "FX_CONVERT"
RailOperationBlock RailOperation = "BLOCK"
RailOperationRelease RailOperation = "RELEASE"
)
type RailOperation = discovery.RailOperation
// RailCapabilities are declared per gateway instance.
type RailCapabilities struct {

View File

@@ -1,26 +1,29 @@
package model
import "strings"
import (
"github.com/tech/sendico/pkg/discovery"
"strings"
)
var supportedRailOperations = map[RailOperation]struct{}{
RailOperationDebit: {},
RailOperationCredit: {},
RailOperationExternalDebit: {},
RailOperationExternalCredit: {},
RailOperationMove: {},
RailOperationSend: {},
RailOperationFee: {},
RailOperationObserveConfirm: {},
RailOperationFXConvert: {},
RailOperationBlock: {},
RailOperationRelease: {},
discovery.RailOperationDebit: {},
discovery.RailOperationCredit: {},
discovery.RailOperationExternalDebit: {},
discovery.RailOperationExternalCredit: {},
discovery.RailOperationMove: {},
discovery.RailOperationSend: {},
discovery.RailOperationFee: {},
discovery.RailOperationObserveConfirm: {},
discovery.RailOperationFXConvert: {},
discovery.RailOperationBlock: {},
discovery.RailOperationRelease: {},
}
// ParseRailOperation canonicalizes string values into a RailOperation token.
func ParseRailOperation(value string) RailOperation {
clean := strings.ToUpper(strings.TrimSpace(value))
if clean == "" {
return RailOperationUnspecified
return discovery.RailOperationUnspecified
}
return RailOperation(clean)
}
@@ -40,7 +43,7 @@ func NormalizeRailOperations(values []RailOperation) []RailOperation {
seen := map[RailOperation]bool{}
for _, value := range values {
op := ParseRailOperation(string(value))
if op == RailOperationUnspecified || !IsSupportedRailOperation(op) || seen[op] {
if op == discovery.RailOperationUnspecified || !IsSupportedRailOperation(op) || seen[op] {
continue
}
seen[op] = true
@@ -67,7 +70,7 @@ func NormalizeRailOperationStrings(values []string) []RailOperation {
// HasRailOperation checks whether ops includes action.
func HasRailOperation(ops []RailOperation, action RailOperation) bool {
want := ParseRailOperation(string(action))
if want == RailOperationUnspecified {
if want == discovery.RailOperationUnspecified {
return false
}
for _, op := range ops {
@@ -82,12 +85,12 @@ func HasRailOperation(ops []RailOperation, action RailOperation) bool {
func RailCapabilitiesFromOperations(ops []RailOperation) RailCapabilities {
normalized := NormalizeRailOperations(ops)
return RailCapabilities{
CanPayIn: HasRailOperation(normalized, RailOperationExternalDebit),
CanPayOut: HasRailOperation(normalized, RailOperationSend) || HasRailOperation(normalized, RailOperationExternalCredit),
CanPayIn: HasRailOperation(normalized, discovery.RailOperationExternalDebit),
CanPayOut: HasRailOperation(normalized, discovery.RailOperationSend) || HasRailOperation(normalized, discovery.RailOperationExternalCredit),
CanReadBalance: false,
CanSendFee: HasRailOperation(normalized, RailOperationFee),
RequiresObserveConfirm: HasRailOperation(normalized, RailOperationObserveConfirm),
CanBlock: HasRailOperation(normalized, RailOperationBlock),
CanRelease: HasRailOperation(normalized, RailOperationRelease),
CanSendFee: HasRailOperation(normalized, discovery.RailOperationFee),
RequiresObserveConfirm: HasRailOperation(normalized, discovery.RailOperationObserveConfirm),
CanBlock: HasRailOperation(normalized, discovery.RailOperationBlock),
CanRelease: HasRailOperation(normalized, discovery.RailOperationRelease),
}
}

View File

@@ -1,6 +1,9 @@
package model
import "testing"
import (
"github.com/tech/sendico/pkg/discovery"
"testing"
)
func TestNormalizeRailOperations(t *testing.T) {
ops := NormalizeRailOperations([]RailOperation{
@@ -13,35 +16,35 @@ func TestNormalizeRailOperations(t *testing.T) {
if len(ops) != 2 {
t.Fatalf("unexpected operations count: got=%d want=2", len(ops))
}
if ops[0] != RailOperationSend {
t.Fatalf("unexpected first operation: got=%q want=%q", ops[0], RailOperationSend)
if ops[0] != discovery.RailOperationSend {
t.Fatalf("unexpected first operation: got=%q want=%q", ops[0], discovery.RailOperationSend)
}
if ops[1] != RailOperationExternalCredit {
t.Fatalf("unexpected second operation: got=%q want=%q", ops[1], RailOperationExternalCredit)
if ops[1] != discovery.RailOperationExternalCredit {
t.Fatalf("unexpected second operation: got=%q want=%q", ops[1], discovery.RailOperationExternalCredit)
}
}
func TestHasRailOperation(t *testing.T) {
ops := []RailOperation{RailOperationSend, RailOperationExternalCredit}
if !HasRailOperation(ops, RailOperationSend) {
ops := []RailOperation{discovery.RailOperationSend, discovery.RailOperationExternalCredit}
if !HasRailOperation(ops, discovery.RailOperationSend) {
t.Fatalf("expected send operation to be present")
}
if !HasRailOperation(ops, " external_credit ") {
t.Fatalf("expected external credit operation to be present")
}
if HasRailOperation(ops, RailOperationObserveConfirm) {
if HasRailOperation(ops, discovery.RailOperationObserveConfirm) {
t.Fatalf("did not expect observe confirm operation to be present")
}
}
func TestRailCapabilitiesFromOperations(t *testing.T) {
cap := RailCapabilitiesFromOperations([]RailOperation{
RailOperationExternalDebit,
RailOperationExternalCredit,
RailOperationFee,
RailOperationObserveConfirm,
RailOperationBlock,
RailOperationRelease,
discovery.RailOperationExternalDebit,
discovery.RailOperationExternalCredit,
discovery.RailOperationFee,
discovery.RailOperationObserveConfirm,
discovery.RailOperationBlock,
discovery.RailOperationRelease,
})
if !cap.CanPayIn {

View File

@@ -1,21 +1,24 @@
package model
import "strings"
import (
"github.com/tech/sendico/pkg/discovery"
"strings"
)
var supportedRails = map[Rail]struct{}{
RailCrypto: {},
RailProviderSettlement: {},
RailLedger: {},
RailCardPayout: {},
RailFiatOnRamp: {},
RailFiatOffRamp: {},
discovery.RailCrypto: {},
discovery.RailProviderSettlement: {},
discovery.RailLedger: {},
discovery.RailCardPayout: {},
discovery.RailFiatOnRamp: {},
discovery.RailFiatOffRamp: {},
}
// ParseRail canonicalizes string values into a Rail token.
func ParseRail(value string) Rail {
clean := strings.ToUpper(strings.TrimSpace(value))
if clean == "" {
return RailUnspecified
return discovery.RailUnspecified
}
clean = strings.ReplaceAll(clean, "-", "_")
clean = strings.ReplaceAll(clean, " ", "_")
@@ -24,20 +27,20 @@ func ParseRail(value string) Rail {
}
switch clean {
case string(RailCrypto), "RAIL_CRYPTO":
return RailCrypto
case string(RailProviderSettlement), "PROVIDER_SETTLEMENT", "RAIL_SETTLEMENT", "RAIL_PROVIDER_SETTLEMENT":
return RailProviderSettlement
case string(RailLedger), "RAIL_LEDGER":
return RailLedger
case string(RailCardPayout), "CARD_PAYOUT", "RAIL_CARD", "RAIL_CARD_PAYOUT":
return RailCardPayout
case string(RailFiatOnRamp), "FIAT_ONRAMP", "RAIL_ONRAMP", "RAIL_FIAT_ONRAMP":
return RailFiatOnRamp
case string(RailFiatOffRamp), "FIAT_OFFRAMP", "RAIL_OFFRAMP", "RAIL_FIAT_OFFRAMP":
return RailFiatOffRamp
case string(discovery.RailCrypto), "RAIL_CRYPTO":
return discovery.RailCrypto
case string(discovery.RailProviderSettlement), "PROVIDER_SETTLEMENT", "RAIL_SETTLEMENT", "RAIL_PROVIDER_SETTLEMENT":
return discovery.RailProviderSettlement
case string(discovery.RailLedger), "RAIL_LEDGER":
return discovery.RailLedger
case string(discovery.RailCardPayout), "CARD_PAYOUT", "RAIL_CARD", "RAIL_CARD_PAYOUT":
return discovery.RailCardPayout
case string(discovery.RailFiatOnRamp), "FIAT_ONRAMP", "RAIL_ONRAMP", "RAIL_FIAT_ONRAMP":
return discovery.RailFiatOnRamp
case string(discovery.RailFiatOffRamp), "FIAT_OFFRAMP", "RAIL_OFFRAMP", "RAIL_FIAT_OFFRAMP":
return discovery.RailFiatOffRamp
default:
return RailUnspecified
return discovery.RailUnspecified
}
}
@@ -49,13 +52,13 @@ func IsSupportedRail(rail Rail) bool {
func normalizeRail(value Rail) Rail {
parsed := ParseRail(string(value))
if parsed != RailUnspecified {
if parsed != discovery.RailUnspecified {
return parsed
}
clean := strings.ToUpper(strings.TrimSpace(string(value)))
if clean == "" {
return RailUnspecified
return discovery.RailUnspecified
}
return Rail(clean)

View File

@@ -1,6 +1,9 @@
package model
import "testing"
import (
"github.com/tech/sendico/pkg/discovery"
"testing"
)
func TestParseRail(t *testing.T) {
cases := []struct {
@@ -8,14 +11,14 @@ func TestParseRail(t *testing.T) {
input string
want Rail
}{
{name: "crypto", input: "crypto", want: RailCrypto},
{name: "settlement canonical", input: "SETTLEMENT", want: RailProviderSettlement},
{name: "settlement legacy", input: "provider_settlement", want: RailProviderSettlement},
{name: "card canonical", input: "card", want: RailCardPayout},
{name: "card legacy", input: "card_payout", want: RailCardPayout},
{name: "onramp", input: "fiat_onramp", want: RailFiatOnRamp},
{name: "offramp", input: "fiat_offramp", want: RailFiatOffRamp},
{name: "unknown", input: "telegram", want: RailUnspecified},
{name: "crypto", input: "crypto", want: discovery.RailCrypto},
{name: "settlement canonical", input: "SETTLEMENT", want: discovery.RailProviderSettlement},
{name: "settlement legacy", input: "provider_settlement", want: discovery.RailProviderSettlement},
{name: "card canonical", input: "card", want: discovery.RailCardPayout},
{name: "card legacy", input: "card_payout", want: discovery.RailCardPayout},
{name: "onramp", input: "fiat_onramp", want: discovery.RailFiatOnRamp},
{name: "offramp", input: "fiat_offramp", want: discovery.RailFiatOffRamp},
{name: "unknown", input: "telegram", want: discovery.RailUnspecified},
}
for _, tc := range cases {

View File

@@ -3,6 +3,7 @@ package store
import (
"context"
"errors"
"github.com/tech/sendico/pkg/discovery"
"strings"
"github.com/tech/sendico/payments/storage"
@@ -65,10 +66,10 @@ func (r *Routes) Create(ctx context.Context, route *model.PaymentRoute) error {
return merrors.InvalidArgument("routesStore: nil route")
}
route.Normalize()
if route.FromRail == "" || route.FromRail == model.RailUnspecified {
if route.FromRail == "" || route.FromRail == discovery.RailUnspecified {
return merrors.InvalidArgument("routesStore: from_rail is required")
}
if route.ToRail == "" || route.ToRail == model.RailUnspecified {
if route.ToRail == "" || route.ToRail == discovery.RailUnspecified {
return merrors.InvalidArgument("routesStore: to_rail is required")
}
if route.ID.IsZero() {
@@ -176,20 +177,20 @@ func normalizedRailFilterValues(rail model.Rail) []string {
return nil
}
if parsed := model.ParseRail(string(rail)); parsed != model.RailUnspecified {
if parsed := model.ParseRail(string(rail)); parsed != discovery.RailUnspecified {
switch parsed {
case model.RailCrypto:
return []string{string(model.RailCrypto), "RAIL_CRYPTO"}
case model.RailProviderSettlement:
return []string{string(model.RailProviderSettlement), "PROVIDER_SETTLEMENT", "RAIL_SETTLEMENT", "RAIL_PROVIDER_SETTLEMENT"}
case model.RailLedger:
return []string{string(model.RailLedger), "RAIL_LEDGER"}
case model.RailCardPayout:
return []string{string(model.RailCardPayout), "CARD_PAYOUT", "RAIL_CARD", "RAIL_CARD_PAYOUT"}
case model.RailFiatOnRamp:
return []string{string(model.RailFiatOnRamp), "FIAT_ONRAMP", "RAIL_ONRAMP", "RAIL_FIAT_ONRAMP"}
case model.RailFiatOffRamp:
return []string{string(model.RailFiatOffRamp), "FIAT_OFFRAMP", "RAIL_OFFRAMP", "RAIL_FIAT_OFFRAMP"}
case discovery.RailCrypto:
return []string{string(discovery.RailCrypto), "RAIL_CRYPTO"}
case discovery.RailProviderSettlement:
return []string{string(discovery.RailProviderSettlement), "PROVIDER_SETTLEMENT", "RAIL_SETTLEMENT", "RAIL_PROVIDER_SETTLEMENT"}
case discovery.RailLedger:
return []string{string(discovery.RailLedger), "RAIL_LEDGER"}
case discovery.RailCardPayout:
return []string{string(discovery.RailCardPayout), "CARD_PAYOUT", "RAIL_CARD", "RAIL_CARD_PAYOUT"}
case discovery.RailFiatOnRamp:
return []string{string(discovery.RailFiatOnRamp), "FIAT_ONRAMP", "RAIL_ONRAMP", "RAIL_FIAT_ONRAMP"}
case discovery.RailFiatOffRamp:
return []string{string(discovery.RailFiatOffRamp), "FIAT_OFFRAMP", "RAIL_OFFRAMP", "RAIL_FIAT_OFFRAMP"}
default:
return []string{string(parsed)}
}