226 lines
6.6 KiB
Go
226 lines
6.6 KiB
Go
package model
|
|
|
|
import (
|
|
"fmt"
|
|
"github.com/tech/sendico/pkg/discovery"
|
|
"strings"
|
|
|
|
"github.com/shopspring/decimal"
|
|
)
|
|
|
|
type GatewayDirection int
|
|
|
|
const (
|
|
GatewayDirectionAny GatewayDirection = iota
|
|
GatewayDirectionOut
|
|
GatewayDirectionIn
|
|
)
|
|
|
|
func (d GatewayDirection) String() string {
|
|
switch d {
|
|
case GatewayDirectionOut:
|
|
return "out"
|
|
case GatewayDirectionIn:
|
|
return "in"
|
|
default:
|
|
return "any"
|
|
}
|
|
}
|
|
|
|
func NoEligibleGatewayMessage(network, currency string, action RailOperation, dir GatewayDirection) string {
|
|
return fmt.Sprintf(
|
|
"plan builder: no eligible gateway found for %s %s %s for direction %s",
|
|
strings.ToUpper(strings.TrimSpace(network)),
|
|
strings.ToUpper(strings.TrimSpace(currency)),
|
|
ParseRailOperation(string(action)),
|
|
dir.String(),
|
|
)
|
|
}
|
|
|
|
func IsGatewayEligible(
|
|
gw *GatewayInstanceDescriptor,
|
|
rail Rail,
|
|
network, currency string,
|
|
action RailOperation,
|
|
dir GatewayDirection,
|
|
amount decimal.Decimal,
|
|
) error {
|
|
if gw == nil {
|
|
return gatewayIneligible(gw, "gateway instance is required")
|
|
}
|
|
if !gw.IsEnabled {
|
|
return gatewayIneligible(gw, "gateway instance is disabled")
|
|
}
|
|
if gw.Rail != rail {
|
|
return gatewayIneligible(gw, fmt.Sprintf("rail mismatch: want %s got %s", rail, gw.Rail))
|
|
}
|
|
if network != "" && gw.Network != "" && !strings.EqualFold(gw.Network, network) {
|
|
return gatewayIneligible(gw, fmt.Sprintf("network mismatch: want %s got %s", network, gw.Network))
|
|
}
|
|
if currency != "" && len(gw.Currencies) > 0 {
|
|
found := false
|
|
for _, c := range gw.Currencies {
|
|
if strings.EqualFold(c, currency) {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
return gatewayIneligible(gw, "currency not supported: "+currency)
|
|
}
|
|
}
|
|
|
|
if !gatewayAllowsAction(gw.Operations, gw.Capabilities, action, dir) {
|
|
return gatewayIneligible(gw, fmt.Sprintf("gateway does not allow action=%s dir=%s", action, dir.String()))
|
|
}
|
|
|
|
if currency != "" {
|
|
if err := amountWithinLimits(gw, gw.Limits, currency, amount, action); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type gatewayIneligibleError struct {
|
|
reason string
|
|
}
|
|
|
|
func (e gatewayIneligibleError) Error() string {
|
|
return e.reason
|
|
}
|
|
|
|
func gatewayIneligible(gw *GatewayInstanceDescriptor, reason string) error {
|
|
if strings.TrimSpace(reason) == "" {
|
|
reason = "gateway instance is not eligible"
|
|
}
|
|
instanceID := ""
|
|
if gw != nil {
|
|
instanceID = gw.InstanceID
|
|
}
|
|
return gatewayIneligibleError{reason: fmt.Sprintf("gateway %s eligibility check error: %s", instanceID, reason)}
|
|
}
|
|
|
|
func gatewayAllowsAction(operations []RailOperation, cap RailCapabilities, action RailOperation, dir GatewayDirection) bool {
|
|
normalized := NormalizeRailOperations(operations)
|
|
if len(normalized) > 0 {
|
|
return operationsAllowAction(normalized, action, dir)
|
|
}
|
|
return capabilityAllowsAction(cap, action, dir)
|
|
}
|
|
|
|
func capabilityAllowsAction(cap RailCapabilities, action RailOperation, dir GatewayDirection) bool {
|
|
switch action {
|
|
case discovery.RailOperationSend:
|
|
switch dir {
|
|
case GatewayDirectionOut:
|
|
return cap.CanPayOut
|
|
case GatewayDirectionIn:
|
|
return cap.CanPayIn
|
|
default:
|
|
return cap.CanPayIn || cap.CanPayOut
|
|
}
|
|
case discovery.RailOperationExternalDebit, discovery.RailOperationExternalCredit:
|
|
switch dir {
|
|
case GatewayDirectionOut:
|
|
return cap.CanPayOut
|
|
case GatewayDirectionIn:
|
|
return cap.CanPayIn
|
|
default:
|
|
return cap.CanPayIn || cap.CanPayOut
|
|
}
|
|
case discovery.RailOperationFee:
|
|
return cap.CanSendFee
|
|
case discovery.RailOperationObserveConfirm:
|
|
return cap.RequiresObserveConfirm
|
|
case discovery.RailOperationBlock:
|
|
return cap.CanBlock
|
|
case discovery.RailOperationRelease:
|
|
return cap.CanRelease
|
|
default:
|
|
return true
|
|
}
|
|
}
|
|
|
|
func operationsAllowAction(operations []RailOperation, action RailOperation, dir GatewayDirection) bool {
|
|
action = ParseRailOperation(string(action))
|
|
if action == discovery.RailOperationUnspecified {
|
|
return false
|
|
}
|
|
|
|
if HasRailOperation(operations, action) {
|
|
return true
|
|
}
|
|
|
|
switch action {
|
|
case discovery.RailOperationSend:
|
|
switch dir {
|
|
case GatewayDirectionIn:
|
|
return HasRailOperation(operations, discovery.RailOperationExternalDebit)
|
|
case GatewayDirectionOut:
|
|
return HasRailOperation(operations, discovery.RailOperationExternalCredit)
|
|
default:
|
|
return HasRailOperation(operations, discovery.RailOperationExternalDebit) ||
|
|
HasRailOperation(operations, discovery.RailOperationExternalCredit)
|
|
}
|
|
case discovery.RailOperationExternalDebit:
|
|
return HasRailOperation(operations, discovery.RailOperationSend)
|
|
case discovery.RailOperationExternalCredit:
|
|
return HasRailOperation(operations, discovery.RailOperationSend)
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func amountWithinLimits(gw *GatewayInstanceDescriptor, limits Limits, currency string, amount decimal.Decimal, action RailOperation) error {
|
|
min := firstLimitValue(limits.MinAmount, "")
|
|
max := firstLimitValue(limits.MaxAmount, "")
|
|
perTxMin := firstLimitValue(limits.PerTxMinAmount, "")
|
|
perTxMax := firstLimitValue(limits.PerTxMaxAmount, "")
|
|
maxFee := firstLimitValue(limits.PerTxMaxFee, "")
|
|
|
|
if override, ok := limits.CurrencyLimits[currency]; ok {
|
|
min = firstLimitValue(override.MinAmount, min)
|
|
max = firstLimitValue(override.MaxAmount, max)
|
|
if action == discovery.RailOperationFee {
|
|
maxFee = firstLimitValue(override.MaxFee, maxFee)
|
|
}
|
|
}
|
|
|
|
if min != "" {
|
|
if val, err := decimal.NewFromString(min); err == nil && amount.LessThan(val) {
|
|
return gatewayIneligible(gw, fmt.Sprintf("amount %s %s below min limit %s", amount.String(), currency, val.String()))
|
|
}
|
|
}
|
|
if perTxMin != "" {
|
|
if val, err := decimal.NewFromString(perTxMin); err == nil && amount.LessThan(val) {
|
|
return gatewayIneligible(gw, fmt.Sprintf("amount %s %s below per-tx min limit %s", amount.String(), currency, val.String()))
|
|
}
|
|
}
|
|
if max != "" {
|
|
if val, err := decimal.NewFromString(max); err == nil && amount.GreaterThan(val) {
|
|
return gatewayIneligible(gw, fmt.Sprintf("amount %s %s exceeds max limit %s", amount.String(), currency, val.String()))
|
|
}
|
|
}
|
|
if perTxMax != "" {
|
|
if val, err := decimal.NewFromString(perTxMax); err == nil && amount.GreaterThan(val) {
|
|
return gatewayIneligible(gw, fmt.Sprintf("amount %s %s exceeds per-tx max limit %s", amount.String(), currency, val.String()))
|
|
}
|
|
}
|
|
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()))
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func firstLimitValue(primary, fallback string) string {
|
|
val := strings.TrimSpace(primary)
|
|
if val != "" {
|
|
return val
|
|
}
|
|
return strings.TrimSpace(fallback)
|
|
}
|