fixed linting config
This commit is contained in:
@@ -4,9 +4,11 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/tech/sendico/gateway/mntx/internal/service/monetix"
|
||||
"github.com/tech/sendico/gateway/mntx/storage"
|
||||
"github.com/tech/sendico/gateway/mntx/storage/model"
|
||||
@@ -15,6 +17,7 @@ import (
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
msg "github.com/tech/sendico/pkg/messaging"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
gatewayv1 "github.com/tech/sendico/pkg/proto/common/gateway/v1"
|
||||
mntxv1 "github.com/tech/sendico/pkg/proto/gateway/mntx/v1"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"go.uber.org/zap"
|
||||
@@ -27,6 +30,9 @@ type cardPayoutProcessor struct {
|
||||
store storage.Repository
|
||||
httpClient *http.Client
|
||||
producer msg.Producer
|
||||
|
||||
perTxMinAmountMinor int64
|
||||
perTxMinAmountMinorByCurrency map[string]int64
|
||||
}
|
||||
|
||||
func mergePayoutStateWithExisting(state, existing *model.CardPayout) {
|
||||
@@ -118,6 +124,90 @@ func newCardPayoutProcessor(
|
||||
}
|
||||
}
|
||||
|
||||
func (p *cardPayoutProcessor) applyGatewayDescriptor(descriptor *gatewayv1.GatewayInstanceDescriptor) {
|
||||
if p == nil {
|
||||
return
|
||||
}
|
||||
minAmountMinor, perCurrency := perTxMinAmountPolicy(descriptor)
|
||||
p.perTxMinAmountMinor = minAmountMinor
|
||||
p.perTxMinAmountMinorByCurrency = perCurrency
|
||||
}
|
||||
|
||||
func perTxMinAmountPolicy(descriptor *gatewayv1.GatewayInstanceDescriptor) (int64, map[string]int64) {
|
||||
if descriptor == nil || descriptor.GetLimits() == nil {
|
||||
return 0, nil
|
||||
}
|
||||
limits := descriptor.GetLimits()
|
||||
globalMin, _ := decimalAmountToMinor(firstNonEmpty(limits.GetPerTxMinAmount(), limits.GetMinAmount()))
|
||||
perCurrency := map[string]int64{}
|
||||
for currency, override := range limits.GetCurrencyLimits() {
|
||||
if override == nil {
|
||||
continue
|
||||
}
|
||||
minor, ok := decimalAmountToMinor(override.GetMinAmount())
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
code := strings.ToUpper(strings.TrimSpace(currency))
|
||||
if code == "" {
|
||||
continue
|
||||
}
|
||||
perCurrency[code] = minor
|
||||
}
|
||||
if len(perCurrency) == 0 {
|
||||
perCurrency = nil
|
||||
}
|
||||
return globalMin, perCurrency
|
||||
}
|
||||
|
||||
func decimalAmountToMinor(raw string) (int64, bool) {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return 0, false
|
||||
}
|
||||
value, err := decimal.NewFromString(raw)
|
||||
if err != nil || !value.IsPositive() {
|
||||
return 0, false
|
||||
}
|
||||
minor := value.Mul(decimal.NewFromInt(100)).Ceil().IntPart()
|
||||
if minor <= 0 {
|
||||
return 0, false
|
||||
}
|
||||
return minor, true
|
||||
}
|
||||
|
||||
func (p *cardPayoutProcessor) validatePerTxMinimum(amountMinor int64, currency string) error {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
minAmountMinor := p.perTxMinimum(currency)
|
||||
if minAmountMinor <= 0 || amountMinor >= minAmountMinor {
|
||||
return nil
|
||||
}
|
||||
return newPayoutError("amount_below_minimum", merrors.InvalidArgument(
|
||||
fmt.Sprintf("amount_minor must be at least %d", minAmountMinor),
|
||||
"amount_minor",
|
||||
))
|
||||
}
|
||||
|
||||
func (p *cardPayoutProcessor) perTxMinimum(currency string) int64 {
|
||||
if p == nil {
|
||||
return 0
|
||||
}
|
||||
minAmountMinor := p.perTxMinAmountMinor
|
||||
if len(p.perTxMinAmountMinorByCurrency) == 0 {
|
||||
return minAmountMinor
|
||||
}
|
||||
code := strings.ToUpper(strings.TrimSpace(currency))
|
||||
if code == "" {
|
||||
return minAmountMinor
|
||||
}
|
||||
if override, ok := p.perTxMinAmountMinorByCurrency[code]; ok && override > 0 {
|
||||
return override
|
||||
}
|
||||
return minAmountMinor
|
||||
}
|
||||
|
||||
func (p *cardPayoutProcessor) Submit(ctx context.Context, req *mntxv1.CardPayoutRequest) (*mntxv1.CardPayoutResponse, error) {
|
||||
if p == nil {
|
||||
return nil, merrors.Internal("card payout processor not initialised")
|
||||
@@ -147,6 +237,17 @@ func (p *cardPayoutProcessor) Submit(ctx context.Context, req *mntxv1.CardPayout
|
||||
)
|
||||
return nil, err
|
||||
}
|
||||
if err := p.validatePerTxMinimum(req.GetAmountMinor(), req.GetCurrency()); err != nil {
|
||||
p.logger.Warn("Card payout amount below configured minimum",
|
||||
zap.String("payout_id", req.GetPayoutId()),
|
||||
zap.String("customer_id", req.GetCustomerId()),
|
||||
zap.Int64("amount_minor", req.GetAmountMinor()),
|
||||
zap.String("currency", strings.ToUpper(strings.TrimSpace(req.GetCurrency()))),
|
||||
zap.Int64("configured_min_amount_minor", p.perTxMinimum(req.GetCurrency())),
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
projectID, err := p.resolveProjectID(req.GetProjectId(), "payout_id", req.GetPayoutId())
|
||||
if err != nil {
|
||||
@@ -257,6 +358,17 @@ func (p *cardPayoutProcessor) SubmitToken(ctx context.Context, req *mntxv1.CardT
|
||||
)
|
||||
return nil, err
|
||||
}
|
||||
if err := p.validatePerTxMinimum(req.GetAmountMinor(), req.GetCurrency()); err != nil {
|
||||
p.logger.Warn("Card token payout amount below configured minimum",
|
||||
zap.String("payout_id", req.GetPayoutId()),
|
||||
zap.String("customer_id", req.GetCustomerId()),
|
||||
zap.Int64("amount_minor", req.GetAmountMinor()),
|
||||
zap.String("currency", strings.ToUpper(strings.TrimSpace(req.GetCurrency()))),
|
||||
zap.Int64("configured_min_amount_minor", p.perTxMinimum(req.GetCurrency())),
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
projectID, err := p.resolveProjectID(req.GetProjectId(), "payout_id", req.GetPayoutId())
|
||||
if err != nil {
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/tech/sendico/gateway/mntx/storage/model"
|
||||
clockpkg "github.com/tech/sendico/pkg/clock"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
gatewayv1 "github.com/tech/sendico/pkg/proto/common/gateway/v1"
|
||||
mntxv1 "github.com/tech/sendico/pkg/proto/gateway/mntx/v1"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
@@ -119,6 +120,63 @@ func TestCardPayoutProcessor_Submit_MissingConfig(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCardPayoutProcessor_Submit_RejectsAmountBelowConfiguredMinimum(t *testing.T) {
|
||||
cfg := monetix.Config{
|
||||
BaseURL: "https://monetix.test",
|
||||
SecretKey: "secret",
|
||||
AllowedCurrencies: []string{"RUB"},
|
||||
}
|
||||
|
||||
repo := newMockRepository()
|
||||
processor := newCardPayoutProcessor(
|
||||
zap.NewNop(),
|
||||
cfg,
|
||||
staticClock{now: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)},
|
||||
repo,
|
||||
&http.Client{},
|
||||
nil,
|
||||
)
|
||||
processor.applyGatewayDescriptor(&gatewayv1.GatewayInstanceDescriptor{
|
||||
Limits: &gatewayv1.Limits{
|
||||
PerTxMinAmount: "20.00",
|
||||
},
|
||||
})
|
||||
|
||||
req := validCardPayoutRequest() // 15.00 RUB
|
||||
_, err := processor.Submit(context.Background(), req)
|
||||
requireReason(t, err, "amount_below_minimum")
|
||||
}
|
||||
|
||||
func TestCardPayoutProcessor_SubmitToken_RejectsAmountBelowCurrencyMinimum(t *testing.T) {
|
||||
cfg := monetix.Config{
|
||||
BaseURL: "https://monetix.test",
|
||||
SecretKey: "secret",
|
||||
AllowedCurrencies: []string{"USD"},
|
||||
}
|
||||
|
||||
repo := newMockRepository()
|
||||
processor := newCardPayoutProcessor(
|
||||
zap.NewNop(),
|
||||
cfg,
|
||||
staticClock{now: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)},
|
||||
repo,
|
||||
&http.Client{},
|
||||
nil,
|
||||
)
|
||||
processor.applyGatewayDescriptor(&gatewayv1.GatewayInstanceDescriptor{
|
||||
Limits: &gatewayv1.Limits{
|
||||
PerTxMinAmount: "20.00",
|
||||
CurrencyLimits: map[string]*gatewayv1.LimitsOverride{
|
||||
"USD": {MinAmount: "30.00"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
req := validCardTokenPayoutRequest() // 25.00 USD
|
||||
_, err := processor.SubmitToken(context.Background(), req)
|
||||
requireReason(t, err, "amount_below_minimum")
|
||||
}
|
||||
|
||||
func TestCardPayoutProcessor_ProcessCallback(t *testing.T) {
|
||||
cfg := monetix.Config{
|
||||
SecretKey: "secret",
|
||||
|
||||
@@ -85,6 +85,7 @@ func NewService(logger mlogger.Logger, opts ...Option) *Service {
|
||||
}
|
||||
|
||||
svc.card = newCardPayoutProcessor(svc.logger, svc.config, svc.clock, svc.storage, svc.httpClient, svc.producer)
|
||||
svc.card.applyGatewayDescriptor(svc.gatewayDescriptor)
|
||||
svc.startDiscoveryAnnouncer()
|
||||
|
||||
return svc
|
||||
@@ -149,44 +150,132 @@ func (s *Service) startDiscoveryAnnouncer() {
|
||||
if id := strings.TrimSpace(s.gatewayDescriptor.GetId()); id != "" {
|
||||
announce.ID = id
|
||||
}
|
||||
announce.Network = strings.TrimSpace(s.gatewayDescriptor.GetNetwork())
|
||||
announce.Currencies = append([]string(nil), s.gatewayDescriptor.GetCurrencies()...)
|
||||
announce.Limits = limitsFromDescriptor(s.gatewayDescriptor.GetLimits())
|
||||
announce.Currencies = currenciesFromDescriptor(s.gatewayDescriptor)
|
||||
}
|
||||
s.announcer = discovery.NewAnnouncer(s.logger, s.producer, string(mservice.MntxGateway), announce)
|
||||
s.announcer.Start()
|
||||
}
|
||||
|
||||
func limitsFromDescriptor(src *gatewayv1.Limits) *discovery.Limits {
|
||||
func currenciesFromDescriptor(src *gatewayv1.GatewayInstanceDescriptor) []discovery.CurrencyAnnouncement {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
limits := &discovery.Limits{
|
||||
MinAmount: strings.TrimSpace(src.GetMinAmount()),
|
||||
MaxAmount: strings.TrimSpace(src.GetMaxAmount()),
|
||||
VolumeLimit: map[string]string{},
|
||||
VelocityLimit: map[string]int{},
|
||||
network := strings.TrimSpace(src.GetNetwork())
|
||||
limitsCfg := src.GetLimits()
|
||||
values := src.GetCurrencies()
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
for key, value := range src.GetVolumeLimit() {
|
||||
k := strings.TrimSpace(key)
|
||||
v := strings.TrimSpace(value)
|
||||
if k == "" || v == "" {
|
||||
seen := map[string]bool{}
|
||||
result := make([]discovery.CurrencyAnnouncement, 0, len(values))
|
||||
for _, value := range values {
|
||||
currency := strings.ToUpper(strings.TrimSpace(value))
|
||||
if currency == "" || seen[currency] {
|
||||
continue
|
||||
}
|
||||
limits.VolumeLimit[k] = v
|
||||
seen[currency] = true
|
||||
result = append(result, discovery.CurrencyAnnouncement{
|
||||
Currency: currency,
|
||||
Network: network,
|
||||
Limits: currencyLimitsFromDescriptor(limitsCfg, currency),
|
||||
})
|
||||
}
|
||||
for key, value := range src.GetVelocityLimit() {
|
||||
k := strings.TrimSpace(key)
|
||||
if k == "" {
|
||||
if len(result) == 0 {
|
||||
return nil
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func currencyLimitsFromDescriptor(src *gatewayv1.Limits, currency string) *discovery.CurrencyLimits {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
amountMin := firstNonEmpty(src.GetPerTxMinAmount(), src.GetMinAmount())
|
||||
amountMax := firstNonEmpty(src.GetPerTxMaxAmount(), src.GetMaxAmount())
|
||||
|
||||
limits := &discovery.CurrencyLimits{}
|
||||
if amountMin != "" || amountMax != "" {
|
||||
limits.Amount = &discovery.CurrencyAmount{
|
||||
Min: amountMin,
|
||||
Max: amountMax,
|
||||
}
|
||||
}
|
||||
|
||||
running := &discovery.CurrencyRunningLimits{}
|
||||
for bucket, max := range src.GetVolumeLimit() {
|
||||
bucket = strings.TrimSpace(bucket)
|
||||
max = strings.TrimSpace(max)
|
||||
if bucket == "" || max == "" {
|
||||
continue
|
||||
}
|
||||
limits.VelocityLimit[k] = int(value)
|
||||
running.Volume = append(running.Volume, discovery.VolumeLimit{
|
||||
Window: discovery.Window{
|
||||
Raw: bucket,
|
||||
Named: bucket,
|
||||
},
|
||||
Max: max,
|
||||
})
|
||||
}
|
||||
if len(limits.VolumeLimit) == 0 {
|
||||
limits.VolumeLimit = nil
|
||||
for bucket, max := range src.GetVelocityLimit() {
|
||||
bucket = strings.TrimSpace(bucket)
|
||||
if bucket == "" || max <= 0 {
|
||||
continue
|
||||
}
|
||||
running.Velocity = append(running.Velocity, discovery.VelocityLimit{
|
||||
Window: discovery.Window{
|
||||
Raw: bucket,
|
||||
Named: bucket,
|
||||
},
|
||||
Max: int(max),
|
||||
})
|
||||
}
|
||||
if len(limits.VelocityLimit) == 0 {
|
||||
limits.VelocityLimit = nil
|
||||
if override := src.GetCurrencyLimits()[strings.ToUpper(strings.TrimSpace(currency))]; override != nil {
|
||||
if min := strings.TrimSpace(override.GetMinAmount()); min != "" {
|
||||
if limits.Amount == nil {
|
||||
limits.Amount = &discovery.CurrencyAmount{}
|
||||
}
|
||||
limits.Amount.Min = min
|
||||
}
|
||||
if max := strings.TrimSpace(override.GetMaxAmount()); max != "" {
|
||||
if limits.Amount == nil {
|
||||
limits.Amount = &discovery.CurrencyAmount{}
|
||||
}
|
||||
limits.Amount.Max = max
|
||||
}
|
||||
if maxVolume := strings.TrimSpace(override.GetMaxVolume()); maxVolume != "" {
|
||||
running.Volume = append(running.Volume, discovery.VolumeLimit{
|
||||
Window: discovery.Window{
|
||||
Raw: "default",
|
||||
Named: "default",
|
||||
},
|
||||
Max: maxVolume,
|
||||
})
|
||||
}
|
||||
if maxOps := int(override.GetMaxOps()); maxOps > 0 {
|
||||
running.Velocity = append(running.Velocity, discovery.VelocityLimit{
|
||||
Window: discovery.Window{
|
||||
Raw: "default",
|
||||
Named: "default",
|
||||
},
|
||||
Max: maxOps,
|
||||
})
|
||||
}
|
||||
}
|
||||
if len(running.Volume) > 0 || len(running.Velocity) > 0 {
|
||||
limits.Running = running
|
||||
}
|
||||
if limits.Amount == nil && limits.Running == nil {
|
||||
return nil
|
||||
}
|
||||
return limits
|
||||
}
|
||||
|
||||
func firstNonEmpty(values ...string) string {
|
||||
for _, value := range values {
|
||||
clean := strings.TrimSpace(value)
|
||||
if clean != "" {
|
||||
return clean
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user