payment quotation v2 + payment orchestration v2 draft
This commit is contained in:
224
api/payments/storage/model/gateway_eligibility.go
Normal file
224
api/payments/storage/model/gateway_eligibility.go
Normal file
@@ -0,0 +1,224 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user