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

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