Fixes + stable gateway ids
This commit is contained in:
144
api/payments/storage/model/gateway_affinity.go
Normal file
144
api/payments/storage/model/gateway_affinity.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package model
|
||||
|
||||
import "strings"
|
||||
|
||||
// SelectGatewayByPreference picks a gateway candidate using persisted affinity hints.
|
||||
// Matching order:
|
||||
// 1) gateway ID + instance ID
|
||||
// 2) invoke URI
|
||||
// 3) gateway ID
|
||||
// 4) instance ID
|
||||
// 5) first candidate fallback
|
||||
func SelectGatewayByPreference(
|
||||
candidates []*GatewayInstanceDescriptor,
|
||||
gatewayID string,
|
||||
instanceID string,
|
||||
invokeURI string,
|
||||
) (*GatewayInstanceDescriptor, string) {
|
||||
if len(candidates) == 0 {
|
||||
return nil, ""
|
||||
}
|
||||
|
||||
gatewayID = strings.TrimSpace(gatewayID)
|
||||
instanceID = strings.TrimSpace(instanceID)
|
||||
invokeURI = strings.TrimSpace(invokeURI)
|
||||
|
||||
if gatewayID != "" && instanceID != "" {
|
||||
for _, entry := range candidates {
|
||||
if entry == nil {
|
||||
continue
|
||||
}
|
||||
if strings.EqualFold(strings.TrimSpace(entry.ID), gatewayID) &&
|
||||
strings.EqualFold(strings.TrimSpace(entry.InstanceID), instanceID) {
|
||||
return entry, "exact"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if invokeURI != "" {
|
||||
for _, entry := range candidates {
|
||||
if entry == nil {
|
||||
continue
|
||||
}
|
||||
if strings.EqualFold(strings.TrimSpace(entry.InvokeURI), invokeURI) {
|
||||
return entry, "invoke_uri"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if gatewayID != "" {
|
||||
for _, entry := range candidates {
|
||||
if entry == nil {
|
||||
continue
|
||||
}
|
||||
if strings.EqualFold(strings.TrimSpace(entry.ID), gatewayID) {
|
||||
return entry, "gateway_id"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if instanceID != "" {
|
||||
for _, entry := range candidates {
|
||||
if entry == nil {
|
||||
continue
|
||||
}
|
||||
if strings.EqualFold(strings.TrimSpace(entry.InstanceID), instanceID) {
|
||||
return entry, "instance_id"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, entry := range candidates {
|
||||
if entry != nil {
|
||||
return entry, "rail_fallback"
|
||||
}
|
||||
}
|
||||
|
||||
return nil, ""
|
||||
}
|
||||
|
||||
// GatewayDescriptorIdentityKey returns a stable dedupe key for gateway entries.
|
||||
// The key is composed from logical gateway ID + instance identity; invoke URI is
|
||||
// used as a fallback identity when instance ID is missing.
|
||||
func GatewayDescriptorIdentityKey(entry *GatewayInstanceDescriptor) string {
|
||||
if entry == nil {
|
||||
return ""
|
||||
}
|
||||
return GatewayIdentityKey(entry.ID, entry.InstanceID, entry.InvokeURI)
|
||||
}
|
||||
|
||||
// GatewayIdentityKey composes a stable identity key from gateway affinity fields.
|
||||
func GatewayIdentityKey(gatewayID string, instanceID string, invokeURI string) string {
|
||||
id := strings.ToLower(strings.TrimSpace(gatewayID))
|
||||
if id == "" {
|
||||
return ""
|
||||
}
|
||||
instance := strings.ToLower(strings.TrimSpace(instanceID))
|
||||
if instance == "" {
|
||||
instance = strings.ToLower(strings.TrimSpace(invokeURI))
|
||||
}
|
||||
if instance == "" {
|
||||
return id
|
||||
}
|
||||
return id + "|" + instance
|
||||
}
|
||||
|
||||
// LessGatewayDescriptor orders gateway descriptors deterministically.
|
||||
func LessGatewayDescriptor(left *GatewayInstanceDescriptor, right *GatewayInstanceDescriptor) bool {
|
||||
if left == nil {
|
||||
return right != nil
|
||||
}
|
||||
if right == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if cmp := compareFolded(left.ID, right.ID); cmp != 0 {
|
||||
return cmp < 0
|
||||
}
|
||||
if cmp := compareFolded(left.InstanceID, right.InstanceID); cmp != 0 {
|
||||
return cmp < 0
|
||||
}
|
||||
if cmp := compareFolded(left.InvokeURI, right.InvokeURI); cmp != 0 {
|
||||
return cmp < 0
|
||||
}
|
||||
if cmp := compareFolded(string(left.Rail), string(right.Rail)); cmp != 0 {
|
||||
return cmp < 0
|
||||
}
|
||||
if cmp := compareFolded(left.Network, right.Network); cmp != 0 {
|
||||
return cmp < 0
|
||||
}
|
||||
return compareFolded(left.Version, right.Version) < 0
|
||||
}
|
||||
|
||||
func compareFolded(left string, right string) int {
|
||||
l := strings.ToLower(strings.TrimSpace(left))
|
||||
r := strings.ToLower(strings.TrimSpace(right))
|
||||
switch {
|
||||
case l < r:
|
||||
return -1
|
||||
case l > r:
|
||||
return 1
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
29
api/payments/storage/model/gateway_affinity_identity_test.go
Normal file
29
api/payments/storage/model/gateway_affinity_identity_test.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package model
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestGatewayIdentityKey(t *testing.T) {
|
||||
if got, want := GatewayIdentityKey(" gw ", "inst-1", "grpc://one"), "gw|inst-1"; got != want {
|
||||
t.Fatalf("unexpected gateway identity key: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := GatewayIdentityKey("gw", "", " grpc://one "), "gw|grpc://one"; got != want {
|
||||
t.Fatalf("unexpected gateway identity key with invoke fallback: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := GatewayIdentityKey(" gw ", "", ""), "gw"; got != want {
|
||||
t.Fatalf("unexpected gateway identity key with id only: got=%q want=%q", got, want)
|
||||
}
|
||||
if got := GatewayIdentityKey("", "inst-1", "grpc://one"); got != "" {
|
||||
t.Fatalf("expected empty key when gateway id missing, got=%q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLessGatewayDescriptor(t *testing.T) {
|
||||
a := &GatewayInstanceDescriptor{ID: "gw", InstanceID: "inst-a", InvokeURI: "grpc://a"}
|
||||
b := &GatewayInstanceDescriptor{ID: "gw", InstanceID: "inst-b", InvokeURI: "grpc://b"}
|
||||
if !LessGatewayDescriptor(a, b) {
|
||||
t.Fatalf("expected inst-a to sort before inst-b")
|
||||
}
|
||||
if LessGatewayDescriptor(b, a) {
|
||||
t.Fatalf("expected inst-b not to sort before inst-a")
|
||||
}
|
||||
}
|
||||
@@ -278,17 +278,19 @@ type ExecutionRefs struct {
|
||||
|
||||
// PaymentStep is an explicit action within a payment plan.
|
||||
type PaymentStep struct {
|
||||
StepID string `bson:"stepId,omitempty" json:"stepId,omitempty"`
|
||||
Rail Rail `bson:"rail" json:"rail"`
|
||||
GatewayID string `bson:"gatewayId,omitempty" json:"gatewayId,omitempty"`
|
||||
InstanceID string `bson:"instanceId,omitempty" json:"instanceId,omitempty"`
|
||||
Action RailOperation `bson:"action" json:"action"`
|
||||
DependsOn []string `bson:"dependsOn,omitempty" json:"dependsOn,omitempty"`
|
||||
CommitPolicy CommitPolicy `bson:"commitPolicy,omitempty" json:"commitPolicy,omitempty"`
|
||||
CommitAfter []string `bson:"commitAfter,omitempty" json:"commitAfter,omitempty"`
|
||||
Amount *paymenttypes.Money `bson:"amount,omitempty" json:"amount,omitempty"`
|
||||
FromRole *account_role.AccountRole `bson:"fromRole,omitempty" json:"fromRole,omitempty"`
|
||||
ToRole *account_role.AccountRole `bson:"toRole,omitempty" json:"toRole,omitempty"`
|
||||
StepID string `bson:"stepId,omitempty" json:"stepId,omitempty"`
|
||||
Rail Rail `bson:"rail" json:"rail"`
|
||||
GatewayID string `bson:"gatewayId,omitempty" json:"gatewayId,omitempty"`
|
||||
InstanceID string `bson:"instanceId,omitempty" json:"instanceId,omitempty"`
|
||||
GatewayInvokeURI string `bson:"gatewayInvokeUri,omitempty" json:"gatewayInvokeUri,omitempty"`
|
||||
Action RailOperation `bson:"action" json:"action"`
|
||||
ReportVisibility ReportVisibility `bson:"reportVisibility,omitempty" json:"reportVisibility,omitempty"`
|
||||
DependsOn []string `bson:"dependsOn,omitempty" json:"dependsOn,omitempty"`
|
||||
CommitPolicy CommitPolicy `bson:"commitPolicy,omitempty" json:"commitPolicy,omitempty"`
|
||||
CommitAfter []string `bson:"commitAfter,omitempty" json:"commitAfter,omitempty"`
|
||||
Amount *paymenttypes.Money `bson:"amount,omitempty" json:"amount,omitempty"`
|
||||
FromRole *account_role.AccountRole `bson:"fromRole,omitempty" json:"fromRole,omitempty"`
|
||||
ToRole *account_role.AccountRole `bson:"toRole,omitempty" json:"toRole,omitempty"`
|
||||
}
|
||||
|
||||
// PaymentPlan captures the ordered list of steps to execute a payment.
|
||||
@@ -311,6 +313,7 @@ type ExecutionStep struct {
|
||||
DestinationRef string `bson:"destinationRef,omitempty" json:"destinationRef,omitempty"`
|
||||
TransferRef string `bson:"transferRef,omitempty" json:"transferRef,omitempty"`
|
||||
OperationRef string `bson:"operationRef,omitempty" json:"operationRef,omitempty"`
|
||||
ReportVisibility ReportVisibility `bson:"reportVisibility,omitempty" json:"reportVisibility,omitempty"`
|
||||
Error string `bson:"error,omitempty" json:"error,omitempty"`
|
||||
State OperationState `bson:"state,omitempty" json:"state,omitempty"`
|
||||
Metadata map[string]string `bson:"metadata,omitempty" json:"metadata,omitempty"`
|
||||
@@ -437,6 +440,7 @@ func (p *Payment) Normalize() {
|
||||
step.SourceWalletRef = strings.TrimSpace(step.SourceWalletRef)
|
||||
step.DestinationRef = strings.TrimSpace(step.DestinationRef)
|
||||
step.TransferRef = strings.TrimSpace(step.TransferRef)
|
||||
step.ReportVisibility = NormalizeReportVisibility(step.ReportVisibility)
|
||||
if step.Metadata != nil {
|
||||
for k, v := range step.Metadata {
|
||||
step.Metadata[k] = strings.TrimSpace(v)
|
||||
@@ -455,7 +459,9 @@ func (p *Payment) Normalize() {
|
||||
step.Rail = Rail(strings.TrimSpace(string(step.Rail)))
|
||||
step.GatewayID = strings.TrimSpace(step.GatewayID)
|
||||
step.InstanceID = strings.TrimSpace(step.InstanceID)
|
||||
step.GatewayInvokeURI = strings.TrimSpace(step.GatewayInvokeURI)
|
||||
step.Action = RailOperation(strings.TrimSpace(string(step.Action)))
|
||||
step.ReportVisibility = NormalizeReportVisibility(step.ReportVisibility)
|
||||
step.CommitPolicy = normalizeCommitPolicy(step.CommitPolicy)
|
||||
step.DependsOn = normalizeStringList(step.DependsOn)
|
||||
step.CommitAfter = normalizeStringList(step.CommitAfter)
|
||||
|
||||
@@ -13,6 +13,7 @@ type OrchestrationStep struct {
|
||||
StepID string `bson:"stepId" json:"stepId"`
|
||||
Rail Rail `bson:"rail" json:"rail"`
|
||||
Operation string `bson:"operation" json:"operation"`
|
||||
ReportVisibility ReportVisibility `bson:"reportVisibility,omitempty" json:"reportVisibility,omitempty"`
|
||||
DependsOn []string `bson:"dependsOn,omitempty" json:"dependsOn,omitempty"`
|
||||
CommitPolicy CommitPolicy `bson:"commitPolicy,omitempty" json:"commitPolicy,omitempty"`
|
||||
CommitAfter []string `bson:"commitAfter,omitempty" json:"commitAfter,omitempty"`
|
||||
@@ -52,6 +53,7 @@ func (t *PaymentPlanTemplate) Normalize() {
|
||||
step.StepID = strings.TrimSpace(step.StepID)
|
||||
step.Rail = Rail(strings.ToUpper(strings.TrimSpace(string(step.Rail))))
|
||||
step.Operation = strings.ToLower(strings.TrimSpace(step.Operation))
|
||||
step.ReportVisibility = NormalizeReportVisibility(step.ReportVisibility)
|
||||
step.CommitPolicy = normalizeCommitPolicy(step.CommitPolicy)
|
||||
step.DependsOn = normalizeStringList(step.DependsOn)
|
||||
step.CommitAfter = normalizeStringList(step.CommitAfter)
|
||||
|
||||
@@ -1,21 +1,14 @@
|
||||
package model
|
||||
|
||||
// QuoteKind captures v2 quote kind metadata for persistence.
|
||||
type QuoteKind string
|
||||
// QuoteState captures v2 quote state metadata for persistence.
|
||||
type QuoteState string
|
||||
|
||||
const (
|
||||
QuoteKindUnspecified QuoteKind = "unspecified"
|
||||
QuoteKindExecutable QuoteKind = "executable"
|
||||
QuoteKindIndicative QuoteKind = "indicative"
|
||||
)
|
||||
|
||||
// QuoteLifecycle captures v2 quote lifecycle metadata for persistence.
|
||||
type QuoteLifecycle string
|
||||
|
||||
const (
|
||||
QuoteLifecycleUnspecified QuoteLifecycle = "unspecified"
|
||||
QuoteLifecycleActive QuoteLifecycle = "active"
|
||||
QuoteLifecycleExpired QuoteLifecycle = "expired"
|
||||
QuoteStateUnspecified QuoteState = "unspecified"
|
||||
QuoteStateIndicative QuoteState = "indicative"
|
||||
QuoteStateExecutable QuoteState = "executable"
|
||||
QuoteStateBlocked QuoteState = "blocked"
|
||||
QuoteStateExpired QuoteState = "expired"
|
||||
)
|
||||
|
||||
// QuoteBlockReason captures v2 non-executability reason for persistence.
|
||||
@@ -34,8 +27,6 @@ const (
|
||||
|
||||
// QuoteStatusV2 stores execution status metadata from quotation v2.
|
||||
type QuoteStatusV2 struct {
|
||||
Kind QuoteKind `bson:"kind,omitempty" json:"kind,omitempty"`
|
||||
Lifecycle QuoteLifecycle `bson:"lifecycle,omitempty" json:"lifecycle,omitempty"`
|
||||
Executable *bool `bson:"executable,omitempty" json:"executable,omitempty"`
|
||||
State QuoteState `bson:"state,omitempty" json:"state,omitempty"`
|
||||
BlockReason QuoteBlockReason `bson:"blockReason,omitempty" json:"blockReason,omitempty"`
|
||||
}
|
||||
|
||||
44
api/payments/storage/model/report_visibility.go
Normal file
44
api/payments/storage/model/report_visibility.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package model
|
||||
|
||||
import "strings"
|
||||
|
||||
// ReportVisibility controls which audience should see a step in reports/timelines.
|
||||
type ReportVisibility string
|
||||
|
||||
const (
|
||||
ReportVisibilityUnspecified ReportVisibility = ""
|
||||
ReportVisibilityHidden ReportVisibility = "hidden"
|
||||
ReportVisibilityUser ReportVisibility = "user"
|
||||
ReportVisibilityBackoffice ReportVisibility = "backoffice"
|
||||
ReportVisibilityAudit ReportVisibility = "audit"
|
||||
)
|
||||
|
||||
// NormalizeReportVisibility trims and lowercases the visibility value.
|
||||
func NormalizeReportVisibility(value ReportVisibility) ReportVisibility {
|
||||
return ReportVisibility(strings.ToLower(strings.TrimSpace(string(value))))
|
||||
}
|
||||
|
||||
// IsValidReportVisibility reports whether the value is a supported enum variant.
|
||||
func IsValidReportVisibility(value ReportVisibility) bool {
|
||||
switch NormalizeReportVisibility(value) {
|
||||
case ReportVisibilityUnspecified,
|
||||
ReportVisibilityHidden,
|
||||
ReportVisibilityUser,
|
||||
ReportVisibilityBackoffice,
|
||||
ReportVisibilityAudit:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// IsUserVisible returns true when the step should be shown to end users.
|
||||
// Unspecified is treated as user-visible for backward compatibility.
|
||||
func (value ReportVisibility) IsUserVisible() bool {
|
||||
switch NormalizeReportVisibility(value) {
|
||||
case ReportVisibilityUnspecified, ReportVisibilityUser:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user