Files
sendico/api/payments/storage/model/gateway_eligibility.go

225 lines
6.3 KiB
Go

package model
import (
"fmt"
"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 RailOperationSend:
switch dir {
case GatewayDirectionOut:
return cap.CanPayOut
case GatewayDirectionIn:
return cap.CanPayIn
default:
return cap.CanPayIn || cap.CanPayOut
}
case RailOperationExternalDebit, RailOperationExternalCredit:
switch dir {
case GatewayDirectionOut:
return cap.CanPayOut
case GatewayDirectionIn:
return cap.CanPayIn
default:
return cap.CanPayIn || cap.CanPayOut
}
case RailOperationFee:
return cap.CanSendFee
case RailOperationObserveConfirm:
return cap.RequiresObserveConfirm
case RailOperationBlock:
return cap.CanBlock
case RailOperationRelease:
return cap.CanRelease
default:
return true
}
}
func operationsAllowAction(operations []RailOperation, action RailOperation, dir GatewayDirection) bool {
action = ParseRailOperation(string(action))
if action == RailOperationUnspecified {
return false
}
if HasRailOperation(operations, action) {
return true
}
switch action {
case RailOperationSend:
switch dir {
case GatewayDirectionIn:
return HasRailOperation(operations, RailOperationExternalDebit)
case GatewayDirectionOut:
return HasRailOperation(operations, RailOperationExternalCredit)
default:
return HasRailOperation(operations, RailOperationExternalDebit) ||
HasRailOperation(operations, RailOperationExternalCredit)
}
case RailOperationExternalDebit:
return HasRailOperation(operations, RailOperationSend)
case RailOperationExternalCredit:
return HasRailOperation(operations, 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 == 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 == 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)
}